Add analyzer modules
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-01-30 00:58:57 +00:00
parent 15bbff8f3a
commit eaebae1833

View 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);
}