Init
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Graph Module
|
||||
*
|
||||
* Provides graph traversal and query functionality for the code knowledge graph.
|
||||
*/
|
||||
|
||||
export { GraphTraverser } from './traversal';
|
||||
export { GraphQueryManager } from './queries';
|
||||
@@ -0,0 +1,416 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
/**
|
||||
* Graph Traversal Algorithms
|
||||
*
|
||||
* BFS and DFS traversal for the code knowledge graph.
|
||||
*/
|
||||
|
||||
import { Node, Edge, Subgraph, TraversalOptions, EdgeKind } from '../types';
|
||||
import { QueryBuilder } from '../db/queries';
|
||||
|
||||
/**
|
||||
* Default traversal options
|
||||
*/
|
||||
const DEFAULT_OPTIONS: Required<TraversalOptions> = {
|
||||
maxDepth: Infinity,
|
||||
edgeKinds: [],
|
||||
nodeKinds: [],
|
||||
direction: 'outgoing',
|
||||
limit: 1000,
|
||||
includeStart: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Result of a single traversal step
|
||||
*/
|
||||
interface TraversalStep {
|
||||
node: Node;
|
||||
edge: Edge | null;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graph traverser for BFS and DFS traversal
|
||||
*/
|
||||
export class GraphTraverser {
|
||||
private queries: QueryBuilder;
|
||||
|
||||
constructor(queries: QueryBuilder) {
|
||||
this.queries = queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse the graph using breadth-first search
|
||||
*
|
||||
* @param startId - Starting node ID
|
||||
* @param options - Traversal options
|
||||
* @returns Subgraph containing traversed nodes and edges
|
||||
*/
|
||||
traverseBFS(startId: string, options: TraversalOptions = {}): Subgraph {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||
const startNode = this.queries.getNodeById(startId);
|
||||
|
||||
if (!startNode) {
|
||||
return { nodes: new Map(), edges: [], roots: [] };
|
||||
}
|
||||
|
||||
const nodes = new Map<string, Node>();
|
||||
const edges: Edge[] = [];
|
||||
const visited = new Set<string>();
|
||||
const queue: TraversalStep[] = [{ node: startNode, edge: null, depth: 0 }];
|
||||
|
||||
if (opts.includeStart) {
|
||||
nodes.set(startNode.id, startNode);
|
||||
}
|
||||
|
||||
while (queue.length > 0 && nodes.size < opts.limit) {
|
||||
const step = queue.shift()!;
|
||||
const { node, edge, depth } = step;
|
||||
|
||||
if (visited.has(node.id)) {
|
||||
continue;
|
||||
}
|
||||
visited.add(node.id);
|
||||
|
||||
// Add edge to result
|
||||
if (edge) {
|
||||
edges.push(edge);
|
||||
}
|
||||
|
||||
// Check depth limit
|
||||
if (depth >= opts.maxDepth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get adjacent edges
|
||||
const adjacentEdges = this.getAdjacentEdges(node.id, opts.direction, opts.edgeKinds);
|
||||
|
||||
for (const adjEdge of adjacentEdges) {
|
||||
const nextNodeId = opts.direction === 'incoming' ? adjEdge.source : adjEdge.target;
|
||||
|
||||
if (visited.has(nextNodeId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextNode = this.queries.getNodeById(nextNodeId);
|
||||
if (!nextNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply node kind filter
|
||||
if (opts.nodeKinds && opts.nodeKinds.length > 0 && !opts.nodeKinds.includes(nextNode.kind)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add node to result
|
||||
nodes.set(nextNode.id, nextNode);
|
||||
|
||||
// Queue for further traversal
|
||||
queue.push({ node: nextNode, edge: adjEdge, depth: depth + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
roots: [startId],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse the graph using depth-first search
|
||||
*
|
||||
* @param startId - Starting node ID
|
||||
* @param options - Traversal options
|
||||
* @returns Subgraph containing traversed nodes and edges
|
||||
*/
|
||||
traverseDFS(startId: string, options: TraversalOptions = {}): Subgraph {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||
const startNode = this.queries.getNodeById(startId);
|
||||
|
||||
if (!startNode) {
|
||||
return { nodes: new Map(), edges: [], roots: [] };
|
||||
}
|
||||
|
||||
const nodes = new Map<string, Node>();
|
||||
const edges: Edge[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
if (opts.includeStart) {
|
||||
nodes.set(startNode.id, startNode);
|
||||
}
|
||||
|
||||
this.dfsRecursive(startNode, 0, opts, nodes, edges, visited);
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
roots: [startId],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive DFS helper
|
||||
*/
|
||||
private dfsRecursive(
|
||||
node: Node,
|
||||
depth: number,
|
||||
opts: Required<TraversalOptions>,
|
||||
nodes: Map<string, Node>,
|
||||
edges: Edge[],
|
||||
visited: Set<string>
|
||||
): void {
|
||||
if (visited.has(node.id) || nodes.size >= opts.limit || depth >= opts.maxDepth) {
|
||||
return;
|
||||
}
|
||||
|
||||
visited.add(node.id);
|
||||
|
||||
// Get adjacent edges
|
||||
const adjacentEdges = this.getAdjacentEdges(node.id, opts.direction, opts.edgeKinds);
|
||||
|
||||
for (const edge of adjacentEdges) {
|
||||
const nextNodeId = opts.direction === 'incoming' ? edge.source : edge.target;
|
||||
|
||||
if (visited.has(nextNodeId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextNode = this.queries.getNodeById(nextNodeId);
|
||||
if (!nextNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply node kind filter
|
||||
if (opts.nodeKinds && opts.nodeKinds.length > 0 && !opts.nodeKinds.includes(nextNode.kind)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add node and edge to result
|
||||
nodes.set(nextNode.id, nextNode);
|
||||
edges.push(edge);
|
||||
|
||||
// Recurse
|
||||
this.dfsRecursive(nextNode, depth + 1, opts, nodes, edges, visited);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get adjacent edges based on direction
|
||||
*/
|
||||
private getAdjacentEdges(
|
||||
nodeId: string,
|
||||
direction: 'outgoing' | 'incoming' | 'both',
|
||||
edgeKinds?: EdgeKind[]
|
||||
): Edge[] {
|
||||
const kinds = edgeKinds && edgeKinds.length > 0 ? edgeKinds : undefined;
|
||||
|
||||
if (direction === 'outgoing') {
|
||||
return this.queries.getOutgoingEdges(nodeId, kinds);
|
||||
} else if (direction === 'incoming') {
|
||||
return this.queries.getIncomingEdges(nodeId, kinds);
|
||||
} else {
|
||||
// Both directions
|
||||
const outgoing = this.queries.getOutgoingEdges(nodeId, kinds);
|
||||
const incoming = this.queries.getIncomingEdges(nodeId, kinds);
|
||||
return [...outgoing, ...incoming];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all callers of a function/method
|
||||
*
|
||||
* @param nodeId - ID of the function/method node
|
||||
* @param maxDepth - Maximum depth to traverse (default: 1)
|
||||
* @returns Array of nodes that call this function
|
||||
*/
|
||||
getCallers(nodeId: string, maxDepth: number = 1): Array<{ node: Node; edge: Edge }> {
|
||||
const result: Array<{ node: Node; edge: Edge }> = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
this.getCallersRecursive(nodeId, maxDepth, 0, result, visited);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getCallersRecursive(
|
||||
nodeId: string,
|
||||
maxDepth: number,
|
||||
currentDepth: number,
|
||||
result: Array<{ node: Node; edge: Edge }>,
|
||||
visited: Set<string>
|
||||
): void {
|
||||
if (currentDepth >= maxDepth || visited.has(nodeId)) {
|
||||
return;
|
||||
}
|
||||
visited.add(nodeId);
|
||||
|
||||
const incomingEdges = this.queries.getIncomingEdges(nodeId, ['calls']);
|
||||
|
||||
for (const edge of incomingEdges) {
|
||||
const callerNode = this.queries.getNodeById(edge.source);
|
||||
if (callerNode && !visited.has(callerNode.id)) {
|
||||
result.push({ node: callerNode, edge });
|
||||
this.getCallersRecursive(callerNode.id, maxDepth, currentDepth + 1, result, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all functions/methods called by a function
|
||||
*
|
||||
* @param nodeId - ID of the function/method node
|
||||
* @param maxDepth - Maximum depth to traverse (default: 1)
|
||||
* @returns Array of nodes called by this function
|
||||
*/
|
||||
getCallees(nodeId: string, maxDepth: number = 1): Array<{ node: Node; edge: Edge }> {
|
||||
const result: Array<{ node: Node; edge: Edge }> = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
this.getCalleesRecursive(nodeId, maxDepth, 0, result, visited);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getCalleesRecursive(
|
||||
nodeId: string,
|
||||
maxDepth: number,
|
||||
currentDepth: number,
|
||||
result: Array<{ node: Node; edge: Edge }>,
|
||||
visited: Set<string>
|
||||
): void {
|
||||
if (currentDepth >= maxDepth || visited.has(nodeId)) {
|
||||
return;
|
||||
}
|
||||
visited.add(nodeId);
|
||||
|
||||
const outgoingEdges = this.queries.getOutgoingEdges(nodeId, ['calls']);
|
||||
|
||||
for (const edge of outgoingEdges) {
|
||||
const calleeNode = this.queries.getNodeById(edge.target);
|
||||
if (calleeNode && !visited.has(calleeNode.id)) {
|
||||
result.push({ node: calleeNode, edge });
|
||||
this.getCalleesRecursive(calleeNode.id, maxDepth, currentDepth + 1, result, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the call graph for a function (both callers and callees)
|
||||
*
|
||||
* @param nodeId - ID of the function/method node
|
||||
* @param depth - Maximum depth in each direction (default: 2)
|
||||
* @returns Subgraph containing the call graph
|
||||
*/
|
||||
getCallGraph(nodeId: string, depth: number = 2): Subgraph {
|
||||
const focalNode = this.queries.getNodeById(nodeId);
|
||||
if (!focalNode) {
|
||||
return { nodes: new Map(), edges: [], roots: [] };
|
||||
}
|
||||
|
||||
const nodes = new Map<string, Node>();
|
||||
const edges: Edge[] = [];
|
||||
|
||||
// Add focal node
|
||||
nodes.set(focalNode.id, focalNode);
|
||||
|
||||
// Get callers
|
||||
const callers = this.getCallers(nodeId, depth);
|
||||
for (const { node, edge } of callers) {
|
||||
nodes.set(node.id, node);
|
||||
edges.push(edge);
|
||||
}
|
||||
|
||||
// Get callees
|
||||
const callees = this.getCallees(nodeId, depth);
|
||||
for (const { node, edge } of callees) {
|
||||
nodes.set(node.id, node);
|
||||
edges.push(edge);
|
||||
}
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
roots: [nodeId],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type hierarchy for a class/interface
|
||||
*
|
||||
* @param nodeId - ID of the class/interface node
|
||||
* @returns Subgraph containing the type hierarchy
|
||||
*/
|
||||
getTypeHierarchy(nodeId: string): Subgraph {
|
||||
const focalNode = this.queries.getNodeById(nodeId);
|
||||
if (!focalNode) {
|
||||
return { nodes: new Map(), edges: [], roots: [] };
|
||||
}
|
||||
|
||||
const nodes = new Map<string, Node>();
|
||||
const edges: Edge[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
// Add focal node
|
||||
nodes.set(focalNode.id, focalNode);
|
||||
|
||||
// Get ancestors (what this extends/implements)
|
||||
this.getTypeAncestors(nodeId, nodes, edges, visited);
|
||||
|
||||
// Get descendants (what extends/implements this)
|
||||
this.getTypeDescendants(nodeId, nodes, edges, visited);
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
roots: [nodeId],
|
||||
};
|
||||
}
|
||||
|
||||
private getTypeAncestors(
|
||||
nodeId: string,
|
||||
nodes: Map<string, Node>,
|
||||
edges: Edge[],
|
||||
visited: Set<string>
|
||||
): void {
|
||||
if (visited.has(nodeId)) {
|
||||
return;
|
||||
}
|
||||
visited.add(nodeId);
|
||||
|
||||
const outgoingEdges = this.queries.getOutgoingEdges(nodeId, ['extends', 'implements']);
|
||||
|
||||
for (const edge of outgoingEdges) {
|
||||
const parentNode = this.queries.getNodeById(edge.target);
|
||||
if (parentNode && !nodes.has(parentNode.id)) {
|
||||
nodes.set(parentNode.id, parentNode);
|
||||
edges.push(edge);
|
||||
this.getTypeAncestors(parentNode.id, nodes, edges, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getTypeDescendants(
|
||||
nodeId: string,
|
||||
nodes: Map<string, Node>,
|
||||
edges: Edge[],
|
||||
visited: Set<string>
|
||||
): void {
|
||||
if (visited.has(nodeId)) {
|
||||
return;
|
||||
}
|
||||
visited.add(nodeId);
|
||||
|
||||
const incomingEdges = this.queries.getIncomingEdges(nodeId, ['extends', 'implements']);
|
||||
|
||||
for (const edge of incomingEdges) {
|
||||
const childNode = this.queries.getNodeById(edge.source);
|
||||
if (childNode && !nodes.has(childNode.id)) {
|
||||
nodes.set(childNode.id, childNode);
|
||||
edges.push(edge);
|
||||
this.getTypeDescendants(childNode.id, nodes, edges, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all usages of a symbol
|
||||
*
|
||||
* @param nodeId - ID of the symbol node
|
||||
* @returns Array of nodes and edges that reference this symbol
|
||||
*/
|
||||
findUsages(nodeId: string): Array<{ node: Node; edge: Edge }> {
|
||||
const result: Array<{ node: Node; edge: Edge }> = [];
|
||||
|
||||
// Get all incoming edges (references, calls, type_of, etc.)
|
||||
const incomingEdges = this.queries.getIncomingEdges(nodeId);
|
||||
|
||||
for (const edge of incomingEdges) {
|
||||
const sourceNode = this.queries.getNodeById(edge.source);
|
||||
if (sourceNode) {
|
||||
result.push({ node: sourceNode, edge });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the impact radius of a node
|
||||
*
|
||||
* Returns all nodes that could be affected by changes to this node.
|
||||
*
|
||||
* @param nodeId - ID of the node
|
||||
* @param maxDepth - Maximum depth to traverse (default: 3)
|
||||
* @returns Subgraph containing potentially impacted nodes
|
||||
*/
|
||||
getImpactRadius(nodeId: string, maxDepth: number = 3): Subgraph {
|
||||
const focalNode = this.queries.getNodeById(nodeId);
|
||||
if (!focalNode) {
|
||||
return { nodes: new Map(), edges: [], roots: [] };
|
||||
}
|
||||
|
||||
const nodes = new Map<string, Node>();
|
||||
const edges: Edge[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
// Add focal node
|
||||
nodes.set(focalNode.id, focalNode);
|
||||
|
||||
// Traverse incoming edges to find all dependents
|
||||
this.getImpactRecursive(nodeId, maxDepth, 0, nodes, edges, visited);
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
roots: [nodeId],
|
||||
};
|
||||
}
|
||||
|
||||
private getImpactRecursive(
|
||||
nodeId: string,
|
||||
maxDepth: number,
|
||||
currentDepth: number,
|
||||
nodes: Map<string, Node>,
|
||||
edges: Edge[],
|
||||
visited: Set<string>
|
||||
): void {
|
||||
if (currentDepth >= maxDepth || visited.has(nodeId)) {
|
||||
return;
|
||||
}
|
||||
visited.add(nodeId);
|
||||
|
||||
// Get all incoming edges (things that depend on this node)
|
||||
const incomingEdges = this.queries.getIncomingEdges(nodeId);
|
||||
|
||||
for (const edge of incomingEdges) {
|
||||
const sourceNode = this.queries.getNodeById(edge.source);
|
||||
if (sourceNode && !nodes.has(sourceNode.id)) {
|
||||
nodes.set(sourceNode.id, sourceNode);
|
||||
edges.push(edge);
|
||||
this.getImpactRecursive(sourceNode.id, maxDepth, currentDepth + 1, nodes, edges, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the shortest path between two nodes
|
||||
*
|
||||
* @param fromId - Starting node ID
|
||||
* @param toId - Target node ID
|
||||
* @param edgeKinds - Edge types to consider (all if empty)
|
||||
* @returns Array of nodes and edges forming the path, or null if no path exists
|
||||
*/
|
||||
findPath(
|
||||
fromId: string,
|
||||
toId: string,
|
||||
edgeKinds: EdgeKind[] = []
|
||||
): Array<{ node: Node; edge: Edge | null }> | null {
|
||||
const fromNode = this.queries.getNodeById(fromId);
|
||||
const toNode = this.queries.getNodeById(toId);
|
||||
|
||||
if (!fromNode || !toNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// BFS to find shortest path
|
||||
const visited = new Set<string>();
|
||||
const queue: Array<{ nodeId: string; path: Array<{ node: Node; edge: Edge | null }> }> = [
|
||||
{ nodeId: fromId, path: [{ node: fromNode, edge: null }] },
|
||||
];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { nodeId, path } = queue.shift()!;
|
||||
|
||||
if (nodeId === toId) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (visited.has(nodeId)) {
|
||||
continue;
|
||||
}
|
||||
visited.add(nodeId);
|
||||
|
||||
// Get outgoing edges
|
||||
const outgoingEdges = this.queries.getOutgoingEdges(
|
||||
nodeId,
|
||||
edgeKinds.length > 0 ? edgeKinds : undefined
|
||||
);
|
||||
|
||||
for (const edge of outgoingEdges) {
|
||||
if (!visited.has(edge.target)) {
|
||||
const nextNode = this.queries.getNodeById(edge.target);
|
||||
if (nextNode) {
|
||||
queue.push({
|
||||
nodeId: edge.target,
|
||||
path: [...path, { node: nextNode, edge }],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // No path found
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the containment hierarchy for a node (ancestors)
|
||||
*
|
||||
* @param nodeId - ID of the node
|
||||
* @returns Array of ancestor nodes from immediate parent to root
|
||||
*/
|
||||
getAncestors(nodeId: string): Node[] {
|
||||
const ancestors: Node[] = [];
|
||||
const visited = new Set<string>();
|
||||
let currentId = nodeId;
|
||||
|
||||
while (true) {
|
||||
if (visited.has(currentId)) {
|
||||
break;
|
||||
}
|
||||
visited.add(currentId);
|
||||
|
||||
// Look for 'contains' edges pointing to this node
|
||||
const containingEdges = this.queries.getIncomingEdges(currentId, ['contains']);
|
||||
|
||||
const firstEdge = containingEdges[0];
|
||||
if (!firstEdge) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Typically there should be at most one containing parent
|
||||
const parentNode = this.queries.getNodeById(firstEdge.source);
|
||||
if (parentNode) {
|
||||
ancestors.push(parentNode);
|
||||
currentId = parentNode.id;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ancestors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get immediate children of a node
|
||||
*
|
||||
* @param nodeId - ID of the node
|
||||
* @returns Array of child nodes
|
||||
*/
|
||||
getChildren(nodeId: string): Node[] {
|
||||
const containsEdges = this.queries.getOutgoingEdges(nodeId, ['contains']);
|
||||
const children: Node[] = [];
|
||||
|
||||
for (const edge of containsEdges) {
|
||||
const childNode = this.queries.getNodeById(edge.target);
|
||||
if (childNode) {
|
||||
children.push(childNode);
|
||||
}
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user