This commit is contained in:
264
src/analyzers/dependencyGraph.ts
Normal file
264
src/analyzers/dependencyGraph.ts
Normal file
@@ -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<string>();
|
||||
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<string, string[]>();
|
||||
|
||||
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<string, string[]>();
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user