diff --git a/src/analyzers/dependencyGraph.ts b/src/analyzers/dependencyGraph.ts new file mode 100644 index 0000000..0cd9372 --- /dev/null +++ b/src/analyzers/dependencyGraph.ts @@ -0,0 +1,264 @@ +import { DependencyGraph, GraphNode, GraphEdge, GraphCluster, AnalysisOptions } from '../types'; +import { ParsedFile, TypeDeclaration } from '../types'; + +export class DependencyGraphBuilder { + private graph: DependencyGraph; + private options: AnalysisOptions; + + constructor(options: AnalysisOptions = {}) { + this.options = { + maxDepth: options.maxDepth ?? 10, + includeExternal: options.includeExternal ?? false, + excludedPatterns: options.excludedPatterns ?? [], + focusTypes: options.focusTypes + }; + + this.graph = { + nodes: new Map(), + edges: [], + clusters: [], + metadata: { + createdAt: new Date().toISOString(), + sourceDirectory: '', + nodeCount: 0, + edgeCount: 0, + analysisOptions: this.options + } + }; + } + + build(files: ParsedFile[], sourceDirectory: string): DependencyGraph { + this.graph.metadata.sourceDirectory = sourceDirectory; + + for (const file of files) { + for (const type of file.types) { + this.addNode(type, file.filePath); + } + } + + for (const file of files) { + for (const type of file.types) { + this.addEdges(type, file.filePath); + } + } + + this.graph.metadata.nodeCount = this.graph.nodes.size; + this.graph.metadata.edgeCount = this.graph.edges.length; + + return this.graph; + } + + private addNode(type: TypeDeclaration, filePath: string): void { + const nodeId = this.getNodeId(type.name, filePath); + + if (this.graph.nodes.has(nodeId)) { + return; + } + + const node: GraphNode = { + id: nodeId, + label: type.name, + kind: type.kind, + filePath, + lineNumber: type.startLine, + metadata: { + endLine: type.endLine, + dependencies: type.dependencies + }, + dependencies: type.dependencies + }; + + this.graph.nodes.set(nodeId, node); + } + + private addEdges(type: TypeDeclaration, filePath: string): void { + const sourceId = this.getNodeId(type.name, filePath); + + for (const dep of type.dependencies) { + const targetId = this.findNodeId(dep); + + if (targetId && sourceId !== targetId) { + const edge: GraphEdge = { + source: sourceId, + target: targetId, + style: 'solid' + }; + + if (!this.edgeExists(sourceId, targetId)) { + this.graph.edges.push(edge); + } + } + } + } + + private getNodeId(name: string, filePath: string): string { + return `${filePath}#${name}`; + } + + private findNodeId(name: string): string | null { + for (const nodeId of this.graph.nodes.keys()) { + if (nodeId.endsWith(`#${name}`)) { + return nodeId; + } + } + return null; + } + + private edgeExists(sourceId: string, targetId: string): boolean { + return this.graph.edges.some( + (edge) => edge.source === sourceId && edge.target === targetId + ); + } + + getNode(nodeId: string): GraphNode | undefined { + return this.graph.nodes.get(nodeId); + } + + getAllNodes(): GraphNode[] { + return Array.from(this.graph.nodes.values()); + } + + getAllEdges(): GraphEdge[] { + return this.graph.edges; + } + + getIncomingEdges(nodeId: string): GraphEdge[] { + return this.graph.edges.filter((edge) => edge.target === nodeId); + } + + getOutgoingEdges(nodeId: string): GraphEdge[] { + return this.graph.edges.filter((edge) => edge.source === nodeId); + } + + getNodeDepth(nodeId: string): number { + const node = this.graph.nodes.get(nodeId); + if (!node) { + return 0; + } + + const outgoingEdges = this.getOutgoingEdges(nodeId); + if (outgoingEdges.length === 0) { + return 0; + } + + let maxDepth = 0; + for (const edge of outgoingEdges) { + const depth = this.getNodeDepth(edge.target) + 1; + if (depth > maxDepth) { + maxDepth = depth; + } + } + + return maxDepth; + } + + findNodeBy(name: string): GraphNode | undefined { + for (const node of this.graph.nodes.values()) { + if (node.label === name) { + return node; + } + } + return undefined; + } + + findNodesByKind(kind: string): GraphNode[] { + return Array.from(this.graph.nodes.values()).filter((node) => node.kind === kind); + } + + getSubgraph(nodeId: string, maxDepth: number): DependencyGraph { + const subgraph: DependencyGraph = { + nodes: new Map(), + edges: [], + clusters: [], + metadata: { + ...this.graph.metadata, + nodeCount: 0, + edgeCount: 0 + } + }; + + const visited = new Set(); + const traverse = (currentId: string, currentDepth: number): void => { + if (visited.has(currentId) || currentDepth > maxDepth) { + return; + } + + visited.add(currentId); + + const node = this.graph.nodes.get(currentId); + if (node) { + subgraph.nodes.set(currentId, node); + + const outgoing = this.getOutgoingEdges(currentId); + for (const edge of outgoing) { + subgraph.edges.push(edge); + traverse(edge.target, currentDepth + 1); + } + } + }; + + traverse(nodeId, 0); + + subgraph.metadata.nodeCount = subgraph.nodes.size; + subgraph.metadata.edgeCount = subgraph.edges.length; + + return subgraph; + } + + clusterByFile(): void { + const fileClusters = new Map(); + + for (const nodeId of this.graph.nodes.keys()) { + const node = this.graph.nodes.get(nodeId); + if (node) { + const filePath = node.filePath; + if (!fileClusters.has(filePath)) { + fileClusters.set(filePath, []); + } + fileClusters.get(filePath)!.push(nodeId); + } + } + + let clusterIndex = 0; + for (const [filePath, nodeIds] of fileClusters) { + const cluster: GraphCluster = { + id: `cluster_${clusterIndex++}`, + label: filePath.split('/').pop() || filePath, + nodeIds, + style: 'filled' + }; + + this.graph.clusters.push(cluster); + } + } + + clusterByKind(): void { + const kindClusters = new Map(); + + for (const nodeId of this.graph.nodes.keys()) { + const node = this.graph.nodes.get(nodeId); + if (node) { + if (!kindClusters.has(node.kind)) { + kindClusters.set(node.kind, []); + } + kindClusters.get(node.kind)!.push(nodeId); + } + } + + let clusterIndex = 0; + for (const [kind, nodeIds] of kindClusters) { + const cluster: GraphCluster = { + id: `cluster_${clusterIndex++}`, + label: `${kind}s`, + nodeIds, + style: 'rounded' + }; + + this.graph.clusters.push(cluster); + } + } +} + +export function createDependencyGraphBuilder(options?: AnalysisOptions): DependencyGraphBuilder { + return new DependencyGraphBuilder(options); +}