import { DependencyGraphBuilder, createDependencyGraphBuilder } from '../src/analyzers/dependencyGraph'; import { CircularDependencyDetector, detectCircularDependencies } from '../src/analyzers/circularDetector'; import { TypeWideningAnalyzer, analyzeTypeWidening } from '../src/analyzers/typeWidening'; import { TypeNarrowingAnalyzer, analyzeTypeNarrowing } from '../src/analyzers/typeNarrowing'; import { DOTExporter, exportToDOT } from '../src/exporters/dotExporter'; import { GraphMLExporter, exportToGraphML } from '../src/exporters/graphmlExporter'; import { JSONExporter, exportToJSON } from '../src/exporters/jsonExporter'; import { DependencyGraph, ParsedFile, GraphNode } from '../src/types'; describe('DependencyGraphBuilder', () => { let builder: ReturnType; beforeEach(() => { builder = createDependencyGraphBuilder(); }); describe('build', () => { it('should create nodes from parsed files', () => { const files: ParsedFile[] = [ { filePath: '/test/a.ts', types: [ { name: 'A', filePath: '/test/a.ts', startLine: 1, endLine: 10, kind: 'interface', dependencies: ['B'], rawNode: undefined } ], imports: [], errors: [] }, { filePath: '/test/b.ts', types: [ { name: 'B', filePath: '/test/b.ts', startLine: 1, endLine: 5, kind: 'interface', dependencies: [], rawNode: undefined } ], imports: [], errors: [] } ]; const graph = builder.build(files, '/test'); expect(graph.nodes.size).toBe(2); }); it('should create edges for dependencies', () => { const files: ParsedFile[] = [ { filePath: '/test/a.ts', types: [ { name: 'A', filePath: '/test/a.ts', startLine: 1, endLine: 10, kind: 'interface', dependencies: ['B'], rawNode: undefined } ], imports: [], errors: [] }, { filePath: '/test/b.ts', types: [ { name: 'B', filePath: '/test/b.ts', startLine: 1, endLine: 5, kind: 'interface', dependencies: [], rawNode: undefined } ], imports: [], errors: [] } ]; const graph = builder.build(files, '/test'); expect(graph.edges).toHaveLength(1); expect(graph.edges[0].source).toContain('A'); expect(graph.edges[0].target).toContain('B'); }); it('should not create duplicate edges', () => { const files: ParsedFile[] = [ { filePath: '/test/a.ts', types: [ { name: 'A', filePath: '/test/a.ts', startLine: 1, endLine: 10, kind: 'interface', dependencies: ['B', 'C'], rawNode: undefined } ], imports: [], errors: [] }, { filePath: '/test/b.ts', types: [ { name: 'B', filePath: '/test/b.ts', startLine: 1, endLine: 5, kind: 'interface', dependencies: [], rawNode: undefined }, { name: 'C', filePath: '/test/b.ts', startLine: 6, endLine: 10, kind: 'interface', dependencies: [], rawNode: undefined } ], imports: [], errors: [] } ]; const graph = builder.build(files, '/test'); expect(graph.edges.length).toBe(2); }); it('should handle self-dependencies gracefully', () => { const files: ParsedFile[] = [ { filePath: '/test/a.ts', types: [ { name: 'A', filePath: '/test/a.ts', startLine: 1, endLine: 10, kind: 'interface', dependencies: ['A'], rawNode: undefined } ], imports: [], errors: [] } ]; const graph = builder.build(files, '/test'); expect(graph.edges).toHaveLength(0); }); }); describe('getNodeDepth', () => { it('should calculate node depth correctly', () => { const files: ParsedFile[] = [ { filePath: '/test/a.ts', types: [ { name: 'A', filePath: '/test/a.ts', startLine: 1, endLine: 10, kind: 'interface', dependencies: ['B'], rawNode: undefined } ], imports: [], errors: [] }, { filePath: '/test/b.ts', types: [ { name: 'B', filePath: '/test/b.ts', startLine: 1, endLine: 5, kind: 'interface', dependencies: ['C'], rawNode: undefined } ], imports: [], errors: [] }, { filePath: '/test/c.ts', types: [ { name: 'C', filePath: '/test/c.ts', startLine: 1, endLine: 5, kind: 'interface', dependencies: [], rawNode: undefined } ], imports: [], errors: [] } ]; const graph = builder.build(files, '/test'); expect(builder.getNodeDepth('/test/a.ts#A')).toBe(2); expect(builder.getNodeDepth('/test/b.ts#B')).toBe(1); expect(builder.getNodeDepth('/test/c.ts#C')).toBe(0); }); }); describe('findNodesByKind', () => { it('should filter nodes by kind', () => { const files: ParsedFile[] = [ { filePath: '/test/a.ts', types: [ { name: 'A', filePath: '/test/a.ts', startLine: 1, endLine: 10, kind: 'interface', dependencies: [], rawNode: undefined }, { name: 'TypeA', filePath: '/test/a.ts', startLine: 11, endLine: 15, kind: 'type_alias', dependencies: [], rawNode: undefined } ], imports: [], errors: [] } ]; const graph = builder.build(files, '/test'); const interfaces = builder.findNodesByKind('interface'); const typeAliases = builder.findNodesByKind('type_alias'); expect(interfaces).toHaveLength(1); expect(typeAliases).toHaveLength(1); }); }); describe('clusterByFile', () => { it('should create clusters for each file', () => { const files: ParsedFile[] = [ { filePath: '/test/a.ts', types: [ { name: 'A', filePath: '/test/a.ts', startLine: 1, endLine: 5, kind: 'interface', dependencies: [], rawNode: undefined } ], imports: [], errors: [] }, { filePath: '/test/b.ts', types: [ { name: 'B', filePath: '/test/b.ts', startLine: 1, endLine: 5, kind: 'interface', dependencies: [], rawNode: undefined } ], imports: [], errors: [] } ]; const graph = builder.build(files, '/test'); builder.clusterByFile(); expect(graph.clusters).toHaveLength(2); }); }); }); describe('CircularDependencyDetector', () => { describe('detect', () => { it('should detect simple circular dependency', () => { const graph: DependencyGraph = { nodes: new Map([ ['A#A', { id: 'A#A', label: 'A', kind: 'interface', filePath: 'a.ts', lineNumber: 1, metadata: {}, dependencies: ['B'] }], ['B#B', { id: 'B#B', label: 'B', kind: 'interface', filePath: 'b.ts', lineNumber: 1, metadata: {}, dependencies: ['A'] }] ]), edges: [ { source: 'A#A', target: 'B#B' }, { source: 'B#B', target: 'A#A' } ], clusters: [], metadata: { createdAt: '', sourceDirectory: '', nodeCount: 2, edgeCount: 2, analysisOptions: {} } }; const cycles = detectCircularDependencies(graph); expect(cycles.length).toBeGreaterThan(0); }); it('should not report false positives for non-circular graphs', () => { const graph: DependencyGraph = { nodes: new Map([ ['A#A', { id: 'A#A', label: 'A', kind: 'interface', filePath: 'a.ts', lineNumber: 1, metadata: {}, dependencies: ['B'] }], ['B#B', { id: 'B#B', label: 'B', kind: 'interface', filePath: 'b.ts', lineNumber: 1, metadata: {}, dependencies: ['C'] }], ['C#C', { id: 'C#C', label: 'C', kind: 'interface', filePath: 'c.ts', lineNumber: 1, metadata: {}, dependencies: [] }] ]), edges: [ { source: 'A#A', target: 'B#B' }, { source: 'B#B', target: 'C#C' } ], clusters: [], metadata: { createdAt: '', sourceDirectory: '', nodeCount: 3, edgeCount: 2, analysisOptions: {} } }; const cycles = detectCircularDependencies(graph); expect(cycles).toHaveLength(0); }); it('should detect complex circular dependencies', () => { const graph: DependencyGraph = { nodes: new Map([ ['A#A', { id: 'A#A', label: 'A', kind: 'interface', filePath: 'a.ts', lineNumber: 1, metadata: {}, dependencies: ['B'] }], ['B#B', { id: 'B#B', label: 'B', kind: 'interface', filePath: 'b.ts', lineNumber: 1, metadata: {}, dependencies: ['C'] }], ['C#C', { id: 'C#C', label: 'C', kind: 'interface', filePath: 'c.ts', lineNumber: 1, metadata: {}, dependencies: ['A'] }] ]), edges: [ { source: 'A#A', target: 'B#B' }, { source: 'B#B', target: 'C#C' }, { source: 'C#C', target: 'A#A' } ], clusters: [], metadata: { createdAt: '', sourceDirectory: '', nodeCount: 3, edgeCount: 3, analysisOptions: {} } }; const cycles = detectCircularDependencies(graph); expect(cycles.length).toBeGreaterThan(0); }); }); }); describe('TypeWideningAnalyzer', () => { describe('analyze', () => { it('should detect large union types', () => { const graph: DependencyGraph = { nodes: new Map([ ['test#A', { id: 'test#A', label: 'A', kind: 'type_alias', filePath: 'test.ts', lineNumber: 1, metadata: { typeAnnotation: 'A | B | C | D | E | F' }, dependencies: [] }] ]), edges: [], clusters: [], metadata: { createdAt: '', sourceDirectory: '', nodeCount: 1, edgeCount: 0, analysisOptions: {} } }; const issues = analyzeTypeWidening(graph); expect(issues.length).toBeGreaterThan(0); }); it('should detect any type usage', () => { const graph: DependencyGraph = { nodes: new Map([ ['test#A', { id: 'test#A', label: 'A', kind: 'type_alias', filePath: 'test.ts', lineNumber: 1, metadata: { typeAnnotation: 'any' }, dependencies: [] }] ]), edges: [], clusters: [], metadata: { createdAt: '', sourceDirectory: '', nodeCount: 1, edgeCount: 0, analysisOptions: {} } }; const issues = analyzeTypeWidening(graph); expect(issues.some(i => i.description.includes('any'))).toBe(true); }); }); }); describe('TypeNarrowingAnalyzer', () => { describe('analyze', () => { it('should detect excessive nesting', () => { const graph: DependencyGraph = { nodes: new Map([ ['test#A', { id: 'test#A', label: 'A', kind: 'type_alias', filePath: 'test.ts', lineNumber: 1, metadata: { typeAnnotation: '(((A)))' }, dependencies: [] }] ]), edges: [], clusters: [], metadata: { createdAt: '', sourceDirectory: '', nodeCount: 1, edgeCount: 0, analysisOptions: {} } }; const issues = analyzeTypeNarrowing(graph); expect(issues.length).toBeGreaterThan(0); }); }); }); describe('DOTExporter', () => { describe('export', () => { it('should generate valid DOT output', () => { const graph: DependencyGraph = { nodes: new Map([ ['test#A', { id: 'test#A', label: 'A', kind: 'interface', filePath: 'test.ts', lineNumber: 1, metadata: {}, dependencies: ['B'] }], ['test#B', { id: 'test#B', label: 'B', kind: 'interface', filePath: 'test.ts', lineNumber: 5, metadata: {}, dependencies: [] }] ]), edges: [{ source: 'test#A', target: 'test#B' }], clusters: [], metadata: { createdAt: '', sourceDirectory: '', nodeCount: 2, edgeCount: 1, analysisOptions: {} } }; const dot = exportToDOT(graph, { format: 'dot', outputPath: 'test.dot' }); expect(dot).toContain('digraph'); expect(dot).toContain('test_A'); expect(dot).toContain('test_B'); expect(dot).toContain('->'); }); }); }); describe('GraphMLExporter', () => { describe('export', () => { it('should generate valid GraphML output', () => { const graph: DependencyGraph = { nodes: new Map([ ['test#A', { id: 'test#A', label: 'A', kind: 'interface', filePath: 'test.ts', lineNumber: 1, metadata: {}, dependencies: [] }] ]), edges: [], clusters: [], metadata: { createdAt: '', sourceDirectory: '', nodeCount: 1, edgeCount: 0, analysisOptions: {} } }; const graphml = exportToGraphML(graph, { format: 'graphml', outputPath: 'test.graphml' }); expect(graphml).toContain('graphml'); expect(graphml).toContain('node'); expect(graphml).toContain('edge'); }); }); }); describe('JSONExporter', () => { describe('export', () => { it('should generate valid JSON output', () => { const graph: DependencyGraph = { nodes: new Map([ ['test#A', { id: 'test#A', label: 'A', kind: 'interface', filePath: 'test.ts', lineNumber: 1, metadata: {}, dependencies: [] }] ]), edges: [], clusters: [], metadata: { createdAt: '', sourceDirectory: '', nodeCount: 1, edgeCount: 0, analysisOptions: {} } }; const json = exportToJSON(graph, { format: 'json', outputPath: 'test.json' }); expect(() => JSON.parse(json)).not.toThrow(); const parsed = JSON.parse(json); expect(parsed.nodes).toHaveLength(1); }); }); });