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

This commit is contained in:
2026-01-30 01:03:06 +00:00
parent 312d0e9782
commit c84d2c25cd

383
test/analyzer.test.ts Normal file
View File

@@ -0,0 +1,383 @@
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<typeof createDependencyGraphBuilder>;
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);
});
});
});