Spaces:
Running
Running
| import { describe, it, expect } from 'vitest'; | |
| import { buildFileNodes, buildFunctionNodes, buildClassNodes, buildGraph, buildImportEdges, buildCallEdges } from './graph-builder'; | |
| function makeFile(overrides = {}) { | |
| return { | |
| path: '/project/src/utils/helper.ts', | |
| relativePath: 'src/utils/helper.ts', | |
| category: 'code', | |
| language: 'typescript', | |
| lineCount: 30, | |
| ...overrides, | |
| }; | |
| } | |
| function makeParseResult(overrides = {}) { | |
| return { | |
| functions: [], | |
| classes: [], | |
| imports: [], | |
| exports: [], | |
| ...overrides, | |
| }; | |
| } | |
| describe('buildFileNodes', () => { | |
| describe('node creation (id, type, name)', () => { | |
| it('creates a node with id set to relativePath', () => { | |
| const files = [makeFile({ relativePath: 'src/index.ts' })]; | |
| const parseResults = new Map(); | |
| const nodes = buildFileNodes(files, parseResults); | |
| expect(nodes).toHaveLength(1); | |
| expect(nodes[0].id).toBe('src/index.ts'); | |
| }); | |
| it('sets type to "file"', () => { | |
| const files = [makeFile()]; | |
| const parseResults = new Map(); | |
| const nodes = buildFileNodes(files, parseResults); | |
| expect(nodes[0].type).toBe('file'); | |
| }); | |
| it('sets name to the file basename', () => { | |
| const files = [makeFile({ relativePath: 'src/utils/helper.ts' })]; | |
| const parseResults = new Map(); | |
| const nodes = buildFileNodes(files, parseResults); | |
| expect(nodes[0].name).toBe('helper.ts'); | |
| }); | |
| it('creates a node for each file', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/a.ts' }), | |
| makeFile({ relativePath: 'src/b.ts' }), | |
| makeFile({ relativePath: 'lib/c.ts' }), | |
| ]; | |
| const parseResults = new Map(); | |
| const nodes = buildFileNodes(files, parseResults); | |
| expect(nodes).toHaveLength(3); | |
| expect(nodes.map(n => n.id)).toEqual(['src/a.ts', 'src/b.ts', 'lib/c.ts']); | |
| }); | |
| }); | |
| describe('summary generation', () => { | |
| it('generates summary from exports (max 5)', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ exports: ['foo', 'bar', 'baz'] })], | |
| ]); | |
| const nodes = buildFileNodes(files, parseResults); | |
| expect(nodes[0].summary).toBe('Exports: foo, bar, baz'); | |
| }); | |
| it('truncates exports beyond 5 with "..."', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const exports = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ exports })], | |
| ]); | |
| const nodes = buildFileNodes(files, parseResults); | |
| expect(nodes[0].summary).toBe('Exports: a, b, c, d, e, ...'); | |
| }); | |
| it('generates summary from classes when no exports', () => { | |
| const files = [makeFile({ relativePath: 'src/model.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/model.ts', makeParseResult({ | |
| classes: [ | |
| { name: 'UserModel', startLine: 1, endLine: 20, methods: [], isExported: false }, | |
| { name: 'PostModel', startLine: 22, endLine: 40, methods: [], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const nodes = buildFileNodes(files, parseResults); | |
| expect(nodes[0].summary).toBe('Classes: UserModel, PostModel'); | |
| }); | |
| it('generates summary from functions when no exports or classes', () => { | |
| const files = [makeFile({ relativePath: 'src/helpers.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/helpers.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'add', startLine: 1, endLine: 3, params: [], isExported: false }, | |
| { name: 'subtract', startLine: 5, endLine: 7, params: [], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const nodes = buildFileNodes(files, parseResults); | |
| expect(nodes[0].summary).toBe('Functions: add, subtract'); | |
| }); | |
| it('falls back to line count and language when parse result is empty', () => { | |
| const files = [makeFile({ relativePath: 'src/empty.ts', lineCount: 10, language: 'typescript' })]; | |
| const parseResults = new Map([ | |
| ['src/empty.ts', makeParseResult()], | |
| ]); | |
| const nodes = buildFileNodes(files, parseResults); | |
| expect(nodes[0].summary).toBe('10 lines of typescript'); | |
| }); | |
| it('falls back to line count and language when no parse result exists', () => { | |
| const files = [makeFile({ relativePath: 'src/data.json', lineCount: 50, language: 'json' })]; | |
| const parseResults = new Map(); | |
| const nodes = buildFileNodes(files, parseResults); | |
| expect(nodes[0].summary).toBe('50 lines of json'); | |
| }); | |
| }); | |
| describe('tag assignment', () => { | |
| it('includes language tag', () => { | |
| const files = [makeFile({ language: 'python' })]; | |
| const nodes = buildFileNodes(files, new Map()); | |
| expect(nodes[0].tags).toContain('python'); | |
| }); | |
| it('includes category tag', () => { | |
| const files = [makeFile({ category: 'config' })]; | |
| const nodes = buildFileNodes(files, new Map()); | |
| expect(nodes[0].tags).toContain('config'); | |
| }); | |
| it('assigns "test" tag for files in test directories', () => { | |
| const testPaths = [ | |
| 'src/test/helper.ts', | |
| 'src/__tests__/utils.ts', | |
| 'spec/app.spec.ts', | |
| ]; | |
| for (const relativePath of testPaths) { | |
| const files = [makeFile({ relativePath })]; | |
| const nodes = buildFileNodes(files, new Map()); | |
| expect(nodes[0].tags).toContain('test'); | |
| } | |
| }); | |
| it('assigns "test" tag for files with .test. or .spec. in name', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.test.ts' })]; | |
| const nodes = buildFileNodes(files, new Map()); | |
| expect(nodes[0].tags).toContain('test'); | |
| }); | |
| it('assigns "component" tag for files in components directory', () => { | |
| const files = [makeFile({ relativePath: 'src/components/Button.tsx' })]; | |
| const nodes = buildFileNodes(files, new Map()); | |
| expect(nodes[0].tags).toContain('component'); | |
| }); | |
| it('assigns "util" tag for files in utils/lib/helpers directories', () => { | |
| const utilPaths = [ | |
| 'src/utils/format.ts', | |
| 'src/lib/crypto.ts', | |
| 'src/helpers/date.ts', | |
| ]; | |
| for (const relativePath of utilPaths) { | |
| const files = [makeFile({ relativePath })]; | |
| const nodes = buildFileNodes(files, new Map()); | |
| expect(nodes[0].tags).toContain('util'); | |
| } | |
| }); | |
| it('assigns "api" tag for files in api/routes/controllers directories', () => { | |
| const apiPaths = [ | |
| 'src/api/users.ts', | |
| 'src/routes/auth.ts', | |
| 'src/controllers/posts.ts', | |
| ]; | |
| for (const relativePath of apiPaths) { | |
| const files = [makeFile({ relativePath })]; | |
| const nodes = buildFileNodes(files, new Map()); | |
| expect(nodes[0].tags).toContain('api'); | |
| } | |
| }); | |
| it('assigns "model" tag for files in models/entities directories', () => { | |
| const modelPaths = [ | |
| 'src/models/User.ts', | |
| 'src/entities/Post.ts', | |
| ]; | |
| for (const relativePath of modelPaths) { | |
| const files = [makeFile({ relativePath })]; | |
| const nodes = buildFileNodes(files, new Map()); | |
| expect(nodes[0].tags).toContain('model'); | |
| } | |
| }); | |
| it('assigns "simple" complexity tag for files under 50 lines', () => { | |
| const files = [makeFile({ lineCount: 30 })]; | |
| const nodes = buildFileNodes(files, new Map()); | |
| expect(nodes[0].tags).toContain('simple'); | |
| }); | |
| it('assigns "moderate" complexity tag for files between 50-200 lines', () => { | |
| const files = [makeFile({ lineCount: 100 })]; | |
| const nodes = buildFileNodes(files, new Map()); | |
| expect(nodes[0].tags).toContain('moderate'); | |
| }); | |
| it('assigns "complex" complexity tag for files over 200 lines', () => { | |
| const files = [makeFile({ lineCount: 500 })]; | |
| const nodes = buildFileNodes(files, new Map()); | |
| expect(nodes[0].tags).toContain('complex'); | |
| }); | |
| it('assigns boundary complexity: 49 lines is simple', () => { | |
| const files = [makeFile({ lineCount: 49 })]; | |
| const nodes = buildFileNodes(files, new Map()); | |
| expect(nodes[0].tags).toContain('simple'); | |
| }); | |
| it('assigns boundary complexity: 50 lines is moderate', () => { | |
| const files = [makeFile({ lineCount: 50 })]; | |
| const nodes = buildFileNodes(files, new Map()); | |
| expect(nodes[0].tags).toContain('moderate'); | |
| }); | |
| it('assigns boundary complexity: 200 lines is moderate', () => { | |
| const files = [makeFile({ lineCount: 200 })]; | |
| const nodes = buildFileNodes(files, new Map()); | |
| expect(nodes[0].tags).toContain('moderate'); | |
| }); | |
| it('assigns boundary complexity: 201 lines is complex', () => { | |
| const files = [makeFile({ lineCount: 201 })]; | |
| const nodes = buildFileNodes(files, new Map()); | |
| expect(nodes[0].tags).toContain('complex'); | |
| }); | |
| }); | |
| describe('empty/edge cases', () => { | |
| it('returns empty array for empty files list', () => { | |
| const nodes = buildFileNodes([], new Map()); | |
| expect(nodes).toEqual([]); | |
| }); | |
| it('handles file with no matching parse result gracefully', () => { | |
| const files = [makeFile({ relativePath: 'src/unknown.ts', lineCount: 5, language: 'typescript' })]; | |
| const parseResults = new Map(); | |
| const nodes = buildFileNodes(files, parseResults); | |
| expect(nodes).toHaveLength(1); | |
| expect(nodes[0].summary).toBe('5 lines of typescript'); | |
| }); | |
| }); | |
| }); | |
| describe('buildGraph', () => { | |
| it('includes file nodes in the output', () => { | |
| const files = [makeFile({ relativePath: 'src/index.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/index.ts', makeParseResult({ exports: ['main'] })], | |
| ]); | |
| const result = buildGraph(files, parseResults); | |
| const fileNodes = result.nodes.filter(n => n.type === 'file'); | |
| expect(fileNodes).toHaveLength(1); | |
| expect(fileNodes[0].id).toBe('src/index.ts'); | |
| expect(fileNodes[0].type).toBe('file'); | |
| }); | |
| it('includes function and class nodes in the output', () => { | |
| const files = [makeFile({ relativePath: 'src/app.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'initialize', startLine: 1, endLine: 20, params: ['config'], isExported: true }, | |
| ], | |
| classes: [ | |
| { name: 'App', startLine: 25, endLine: 80, methods: ['start', 'stop'], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildGraph(files, parseResults); | |
| expect(result.nodes.some(n => n.type === 'function' && n.id === 'src/app.ts::initialize')).toBe(true); | |
| expect(result.nodes.some(n => n.type === 'class' && n.id === 'src/app.ts::App')).toBe(true); | |
| }); | |
| it('includes contains edges for functions and classes', () => { | |
| const files = [makeFile({ relativePath: 'src/app.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'run', startLine: 1, endLine: 15, params: [], isExported: true }, | |
| ], | |
| classes: [ | |
| { name: 'Server', startLine: 20, endLine: 50, methods: ['listen'], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildGraph(files, parseResults); | |
| expect(result.edges).toContainEqual({ | |
| source: 'src/app.ts', | |
| target: 'src/app.ts::run', | |
| type: 'contains', | |
| }); | |
| expect(result.edges).toContainEqual({ | |
| source: 'src/app.ts', | |
| target: 'src/app.ts::Server', | |
| type: 'contains', | |
| }); | |
| }); | |
| it('returns empty nodes and edges for empty input', () => { | |
| const result = buildGraph([], new Map()); | |
| expect(result.nodes).toEqual([]); | |
| expect(result.edges).toEqual([]); | |
| }); | |
| }); | |
| describe('buildFunctionNodes', () => { | |
| describe('significance filtering', () => { | |
| it('creates node for exported function (even if under 10 lines)', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'add', startLine: 1, endLine: 5, params: ['a', 'b'], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildFunctionNodes(files, parseResults); | |
| expect(result.nodes).toHaveLength(1); | |
| expect(result.nodes[0].name).toBe('add'); | |
| }); | |
| it('creates node for function with 10+ lines (even if not exported)', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'processData', startLine: 1, endLine: 15, params: ['data'], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildFunctionNodes(files, parseResults); | |
| expect(result.nodes).toHaveLength(1); | |
| expect(result.nodes[0].name).toBe('processData'); | |
| }); | |
| it('skips function under 10 lines and not exported', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'tiny', startLine: 1, endLine: 5, params: [], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildFunctionNodes(files, parseResults); | |
| expect(result.nodes).toHaveLength(0); | |
| expect(result.edges).toHaveLength(0); | |
| }); | |
| it('boundary: function with exactly 10 lines is significant', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'borderline', startLine: 1, endLine: 10, params: [], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildFunctionNodes(files, parseResults); | |
| expect(result.nodes).toHaveLength(1); | |
| }); | |
| it('boundary: function with 9 lines and not exported is skipped', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'almostSignificant', startLine: 1, endLine: 9, params: [], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildFunctionNodes(files, parseResults); | |
| expect(result.nodes).toHaveLength(0); | |
| }); | |
| }); | |
| describe('node properties', () => { | |
| it('sets id to "{relativePath}::{functionName}"', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'formatDate', startLine: 1, endLine: 15, params: ['date'], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildFunctionNodes(files, parseResults); | |
| expect(result.nodes[0].id).toBe('src/utils.ts::formatDate'); | |
| }); | |
| it('sets type to "function"', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'run', startLine: 1, endLine: 20, params: [], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildFunctionNodes(files, parseResults); | |
| expect(result.nodes[0].type).toBe('function'); | |
| }); | |
| it('generates summary with params and line count', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'formatDate', startLine: 1, endLine: 15, params: ['name', 'age'], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildFunctionNodes(files, parseResults); | |
| expect(result.nodes[0].summary).toBe('(name, age) → 15 lines'); | |
| }); | |
| it('generates summary with empty params', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'init', startLine: 1, endLine: 12, params: [], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildFunctionNodes(files, parseResults); | |
| expect(result.nodes[0].summary).toBe('() → 12 lines'); | |
| }); | |
| }); | |
| describe('tags', () => { | |
| it('includes parent file language tag', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts', language: 'typescript' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'run', startLine: 1, endLine: 20, params: [], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildFunctionNodes(files, parseResults); | |
| expect(result.nodes[0].tags).toContain('typescript'); | |
| }); | |
| it('includes "exported" tag for exported functions', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'run', startLine: 1, endLine: 20, params: [], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildFunctionNodes(files, parseResults); | |
| expect(result.nodes[0].tags).toContain('exported'); | |
| }); | |
| it('does not include "exported" tag for non-exported functions', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'bigHelper', startLine: 1, endLine: 20, params: [], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildFunctionNodes(files, parseResults); | |
| expect(result.nodes[0].tags).not.toContain('exported'); | |
| }); | |
| }); | |
| describe('contains edges', () => { | |
| it('creates contains edge from file to function', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'formatDate', startLine: 1, endLine: 15, params: [], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildFunctionNodes(files, parseResults); | |
| expect(result.edges).toHaveLength(1); | |
| expect(result.edges[0]).toEqual({ | |
| source: 'src/utils.ts', | |
| target: 'src/utils.ts::formatDate', | |
| type: 'contains', | |
| }); | |
| }); | |
| it('creates multiple edges for multiple significant functions', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'funcA', startLine: 1, endLine: 15, params: [], isExported: true }, | |
| { name: 'funcB', startLine: 20, endLine: 35, params: [], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildFunctionNodes(files, parseResults); | |
| expect(result.edges).toHaveLength(2); | |
| expect(result.edges[0].target).toBe('src/utils.ts::funcA'); | |
| expect(result.edges[1].target).toBe('src/utils.ts::funcB'); | |
| }); | |
| }); | |
| describe('edge cases', () => { | |
| it('returns empty results for empty files list', () => { | |
| const result = buildFunctionNodes([], new Map()); | |
| expect(result.nodes).toEqual([]); | |
| expect(result.edges).toEqual([]); | |
| }); | |
| it('returns empty results when no parse results exist', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const result = buildFunctionNodes(files, new Map()); | |
| expect(result.nodes).toEqual([]); | |
| expect(result.edges).toEqual([]); | |
| }); | |
| it('returns empty results when parse result has no functions', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ functions: [] })], | |
| ]); | |
| const result = buildFunctionNodes(files, parseResults); | |
| expect(result.nodes).toEqual([]); | |
| expect(result.edges).toEqual([]); | |
| }); | |
| }); | |
| }); | |
| describe('buildClassNodes', () => { | |
| describe('node creation', () => { | |
| it('creates node for every class (no significance filter)', () => { | |
| const files = [makeFile({ relativePath: 'src/models/User.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/models/User.ts', makeParseResult({ | |
| classes: [ | |
| { name: 'UserModel', startLine: 1, endLine: 20, methods: ['save', 'delete'], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.nodes).toHaveLength(1); | |
| expect(result.nodes[0].name).toBe('UserModel'); | |
| }); | |
| it('creates nodes for multiple classes in one file', () => { | |
| const files = [makeFile({ relativePath: 'src/models.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/models.ts', makeParseResult({ | |
| classes: [ | |
| { name: 'User', startLine: 1, endLine: 10, methods: [], isExported: true }, | |
| { name: 'Post', startLine: 12, endLine: 25, methods: ['publish'], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.nodes).toHaveLength(2); | |
| expect(result.nodes[0].name).toBe('User'); | |
| expect(result.nodes[1].name).toBe('Post'); | |
| }); | |
| }); | |
| describe('node properties', () => { | |
| it('sets id to "{relativePath}::{className}"', () => { | |
| const files = [makeFile({ relativePath: 'src/models/User.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/models/User.ts', makeParseResult({ | |
| classes: [ | |
| { name: 'UserModel', startLine: 1, endLine: 20, methods: [], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.nodes[0].id).toBe('src/models/User.ts::UserModel'); | |
| }); | |
| it('sets type to "class"', () => { | |
| const files = [makeFile({ relativePath: 'src/models.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/models.ts', makeParseResult({ | |
| classes: [ | |
| { name: 'Base', startLine: 1, endLine: 5, methods: [], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.nodes[0].type).toBe('class'); | |
| }); | |
| it('generates summary with methods list', () => { | |
| const files = [makeFile({ relativePath: 'src/service.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/service.ts', makeParseResult({ | |
| classes: [ | |
| { name: 'ApiService', startLine: 1, endLine: 50, methods: ['get', 'post', 'delete'], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.nodes[0].summary).toBe('Methods: get, post, delete'); | |
| }); | |
| it('generates "Empty class" summary when no methods', () => { | |
| const files = [makeFile({ relativePath: 'src/models.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/models.ts', makeParseResult({ | |
| classes: [ | |
| { name: 'EmptyBase', startLine: 1, endLine: 3, methods: [], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.nodes[0].summary).toBe('Empty class'); | |
| }); | |
| }); | |
| describe('tags', () => { | |
| it('includes parent file language tag', () => { | |
| const files = [makeFile({ relativePath: 'src/app.py', language: 'python' })]; | |
| const parseResults = new Map([ | |
| ['src/app.py', makeParseResult({ | |
| classes: [ | |
| { name: 'App', startLine: 1, endLine: 20, methods: ['run'], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.nodes[0].tags).toContain('python'); | |
| }); | |
| it('includes "exported" tag for exported classes', () => { | |
| const files = [makeFile({ relativePath: 'src/models.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/models.ts', makeParseResult({ | |
| classes: [ | |
| { name: 'User', startLine: 1, endLine: 20, methods: ['save'], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.nodes[0].tags).toContain('exported'); | |
| }); | |
| it('does not include "exported" tag for non-exported classes', () => { | |
| const files = [makeFile({ relativePath: 'src/models.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/models.ts', makeParseResult({ | |
| classes: [ | |
| { name: 'Internal', startLine: 1, endLine: 20, methods: ['run'], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.nodes[0].tags).not.toContain('exported'); | |
| }); | |
| it('assigns "small-class" tag for ≤3 methods', () => { | |
| const files = [makeFile({ relativePath: 'src/models.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/models.ts', makeParseResult({ | |
| classes: [ | |
| { name: 'Small', startLine: 1, endLine: 20, methods: ['a', 'b', 'c'], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.nodes[0].tags).toContain('small-class'); | |
| }); | |
| it('assigns "medium-class" tag for 4-10 methods', () => { | |
| const files = [makeFile({ relativePath: 'src/models.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/models.ts', makeParseResult({ | |
| classes: [ | |
| { name: 'Medium', startLine: 1, endLine: 50, methods: ['a', 'b', 'c', 'd', 'e'], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.nodes[0].tags).toContain('medium-class'); | |
| }); | |
| it('assigns "large-class" tag for >10 methods', () => { | |
| const files = [makeFile({ relativePath: 'src/models.ts' })]; | |
| const methods = Array.from({ length: 11 }, (_, i) => `method${i}`); | |
| const parseResults = new Map([ | |
| ['src/models.ts', makeParseResult({ | |
| classes: [ | |
| { name: 'Large', startLine: 1, endLine: 200, methods, isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.nodes[0].tags).toContain('large-class'); | |
| }); | |
| it('boundary: 0 methods is "small-class"', () => { | |
| const files = [makeFile({ relativePath: 'src/models.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/models.ts', makeParseResult({ | |
| classes: [ | |
| { name: 'Empty', startLine: 1, endLine: 3, methods: [], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.nodes[0].tags).toContain('small-class'); | |
| }); | |
| it('boundary: 10 methods is "medium-class"', () => { | |
| const files = [makeFile({ relativePath: 'src/models.ts' })]; | |
| const methods = Array.from({ length: 10 }, (_, i) => `m${i}`); | |
| const parseResults = new Map([ | |
| ['src/models.ts', makeParseResult({ | |
| classes: [ | |
| { name: 'Boundary', startLine: 1, endLine: 100, methods, isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.nodes[0].tags).toContain('medium-class'); | |
| }); | |
| }); | |
| describe('contains edges', () => { | |
| it('creates contains edge from file to class', () => { | |
| const files = [makeFile({ relativePath: 'src/models/User.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/models/User.ts', makeParseResult({ | |
| classes: [ | |
| { name: 'UserModel', startLine: 1, endLine: 20, methods: ['save'], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.edges).toHaveLength(1); | |
| expect(result.edges[0]).toEqual({ | |
| source: 'src/models/User.ts', | |
| target: 'src/models/User.ts::UserModel', | |
| type: 'contains', | |
| }); | |
| }); | |
| }); | |
| describe('edge cases', () => { | |
| it('returns empty results for empty files list', () => { | |
| const result = buildClassNodes([], new Map()); | |
| expect(result.nodes).toEqual([]); | |
| expect(result.edges).toEqual([]); | |
| }); | |
| it('returns empty results when no parse results exist', () => { | |
| const files = [makeFile({ relativePath: 'src/models.ts' })]; | |
| const result = buildClassNodes(files, new Map()); | |
| expect(result.nodes).toEqual([]); | |
| expect(result.edges).toEqual([]); | |
| }); | |
| it('returns empty results when parse result has no classes', () => { | |
| const files = [makeFile({ relativePath: 'src/utils.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/utils.ts', makeParseResult({ classes: [] })], | |
| ]); | |
| const result = buildClassNodes(files, parseResults); | |
| expect(result.nodes).toEqual([]); | |
| expect(result.edges).toEqual([]); | |
| }); | |
| }); | |
| }); | |
| describe('buildImportEdges', () => { | |
| describe('relative import resolution', () => { | |
| it('creates an imports edge for a resolved relative import', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/utils.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './utils', names: ['formatDate'], resolvedPath: undefined }], | |
| })], | |
| ['src/utils.ts', makeParseResult()], | |
| ]); | |
| const edges = buildImportEdges(files, parseResults); | |
| expect(edges).toHaveLength(1); | |
| expect(edges[0]).toEqual({ | |
| source: 'src/app.ts', | |
| target: 'src/utils.ts', | |
| type: 'imports', | |
| }); | |
| }); | |
| it('resolves parent directory imports (..)', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/components/Button.ts' }), | |
| makeFile({ relativePath: 'src/utils.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/components/Button.ts', makeParseResult({ | |
| imports: [{ source: '../utils', names: [], resolvedPath: undefined }], | |
| })], | |
| ['src/utils.ts', makeParseResult()], | |
| ]); | |
| const edges = buildImportEdges(files, parseResults); | |
| expect(edges).toHaveLength(1); | |
| expect(edges[0]).toEqual({ | |
| source: 'src/components/Button.ts', | |
| target: 'src/utils.ts', | |
| type: 'imports', | |
| }); | |
| }); | |
| it('resolves imports with explicit extension', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/config.json' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './config.json', names: [], resolvedPath: undefined }], | |
| })], | |
| ['src/config.json', makeParseResult()], | |
| ]); | |
| const edges = buildImportEdges(files, parseResults); | |
| expect(edges).toHaveLength(1); | |
| expect(edges[0].target).toBe('src/config.json'); | |
| }); | |
| }); | |
| describe('extension resolution', () => { | |
| it('resolves .ts extension', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/helper.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './helper', names: [], resolvedPath: undefined }], | |
| })], | |
| ['src/helper.ts', makeParseResult()], | |
| ]); | |
| const edges = buildImportEdges(files, parseResults); | |
| expect(edges).toHaveLength(1); | |
| expect(edges[0].target).toBe('src/helper.ts'); | |
| }); | |
| it('resolves .js extension', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/legacy.js' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './legacy', names: [], resolvedPath: undefined }], | |
| })], | |
| ['src/legacy.js', makeParseResult()], | |
| ]); | |
| const edges = buildImportEdges(files, parseResults); | |
| expect(edges).toHaveLength(1); | |
| expect(edges[0].target).toBe('src/legacy.js'); | |
| }); | |
| it('resolves .tsx extension', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/Button.tsx' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './Button', names: [], resolvedPath: undefined }], | |
| })], | |
| ['src/Button.tsx', makeParseResult()], | |
| ]); | |
| const edges = buildImportEdges(files, parseResults); | |
| expect(edges).toHaveLength(1); | |
| expect(edges[0].target).toBe('src/Button.tsx'); | |
| }); | |
| it('resolves index.ts for directory imports', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/utils/index.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './utils', names: [], resolvedPath: undefined }], | |
| })], | |
| ['src/utils/index.ts', makeParseResult()], | |
| ]); | |
| const edges = buildImportEdges(files, parseResults); | |
| expect(edges).toHaveLength(1); | |
| expect(edges[0].target).toBe('src/utils/index.ts'); | |
| }); | |
| it('resolves index.js for directory imports', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/lib/index.js' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './lib', names: [], resolvedPath: undefined }], | |
| })], | |
| ['src/lib/index.js', makeParseResult()], | |
| ]); | |
| const edges = buildImportEdges(files, parseResults); | |
| expect(edges).toHaveLength(1); | |
| expect(edges[0].target).toBe('src/lib/index.js'); | |
| }); | |
| }); | |
| describe('external imports are skipped', () => { | |
| it('skips imports not starting with . or ..', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [ | |
| { source: 'react', names: ['useState'], resolvedPath: undefined }, | |
| { source: '@types/node', names: [], resolvedPath: undefined }, | |
| { source: 'lodash/debounce', names: ['debounce'], resolvedPath: undefined }, | |
| ], | |
| })], | |
| ]); | |
| const edges = buildImportEdges(files, parseResults); | |
| expect(edges).toHaveLength(0); | |
| }); | |
| }); | |
| describe('unresolvable imports are skipped', () => { | |
| it('skips relative imports that cannot be resolved to a known file', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [ | |
| { source: './nonexistent', names: [], resolvedPath: undefined }, | |
| { source: '../missing/module', names: [], resolvedPath: undefined }, | |
| ], | |
| })], | |
| ]); | |
| const edges = buildImportEdges(files, parseResults); | |
| expect(edges).toHaveLength(0); | |
| }); | |
| }); | |
| describe('edge cases', () => { | |
| it('returns empty array when no files have imports', () => { | |
| const files = [makeFile({ relativePath: 'src/app.ts' })]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ imports: [] })], | |
| ]); | |
| const edges = buildImportEdges(files, parseResults); | |
| expect(edges).toEqual([]); | |
| }); | |
| it('returns empty array for empty files list', () => { | |
| const edges = buildImportEdges([], new Map()); | |
| expect(edges).toEqual([]); | |
| }); | |
| it('handles file with no parse result', () => { | |
| const files = [makeFile({ relativePath: 'src/app.ts' })]; | |
| const edges = buildImportEdges(files, new Map()); | |
| expect(edges).toEqual([]); | |
| }); | |
| it('creates multiple import edges from one file', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/utils.ts' }), | |
| makeFile({ relativePath: 'src/config.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [ | |
| { source: './utils', names: [], resolvedPath: undefined }, | |
| { source: './config', names: [], resolvedPath: undefined }, | |
| ], | |
| })], | |
| ['src/utils.ts', makeParseResult()], | |
| ['src/config.ts', makeParseResult()], | |
| ]); | |
| const edges = buildImportEdges(files, parseResults); | |
| expect(edges).toHaveLength(2); | |
| expect(edges[0].target).toBe('src/utils.ts'); | |
| expect(edges[1].target).toBe('src/config.ts'); | |
| }); | |
| it('does not create duplicate edges for same import', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/utils.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './utils', names: ['a', 'b'], resolvedPath: undefined }], | |
| })], | |
| ['src/utils.ts', makeParseResult()], | |
| ]); | |
| const edges = buildImportEdges(files, parseResults); | |
| // Only one imports edge per import statement | |
| expect(edges).toHaveLength(1); | |
| }); | |
| }); | |
| }); | |
| describe('buildCallEdges', () => { | |
| describe('call edge creation from named imports', () => { | |
| it('creates a calls edge when named import matches a significant function', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/utils.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './utils', names: ['formatDate'], resolvedPath: undefined }], | |
| })], | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'formatDate', startLine: 1, endLine: 15, params: ['date'], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const edges = buildCallEdges(files, parseResults); | |
| expect(edges).toHaveLength(1); | |
| expect(edges[0]).toEqual({ | |
| source: 'src/app.ts', | |
| target: 'src/utils.ts::formatDate', | |
| type: 'calls', | |
| }); | |
| }); | |
| it('creates multiple call edges for multiple named imports', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/utils.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './utils', names: ['formatDate', 'parseDate'], resolvedPath: undefined }], | |
| })], | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'formatDate', startLine: 1, endLine: 15, params: ['date'], isExported: true }, | |
| { name: 'parseDate', startLine: 20, endLine: 35, params: ['str'], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const edges = buildCallEdges(files, parseResults); | |
| expect(edges).toHaveLength(2); | |
| expect(edges[0].target).toBe('src/utils.ts::formatDate'); | |
| expect(edges[1].target).toBe('src/utils.ts::parseDate'); | |
| }); | |
| it('skips named imports that do not match a significant function', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/utils.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './utils', names: ['CONSTANT', 'SomeType'], resolvedPath: undefined }], | |
| })], | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'formatDate', startLine: 1, endLine: 15, params: ['date'], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const edges = buildCallEdges(files, parseResults); | |
| expect(edges).toHaveLength(0); | |
| }); | |
| it('only matches significant functions (exported or 10+ lines)', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/utils.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './utils', names: ['tiny'], resolvedPath: undefined }], | |
| })], | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| // Not significant: not exported and under 10 lines | |
| { name: 'tiny', startLine: 1, endLine: 5, params: [], isExported: false }, | |
| ], | |
| })], | |
| ]); | |
| const edges = buildCallEdges(files, parseResults); | |
| expect(edges).toHaveLength(0); | |
| }); | |
| }); | |
| describe('external and unresolvable imports', () => { | |
| it('skips external package imports', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: 'lodash', names: ['debounce'], resolvedPath: undefined }], | |
| })], | |
| ]); | |
| const edges = buildCallEdges(files, parseResults); | |
| expect(edges).toHaveLength(0); | |
| }); | |
| it('skips imports that cannot be resolved', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './nonexistent', names: ['foo'], resolvedPath: undefined }], | |
| })], | |
| ]); | |
| const edges = buildCallEdges(files, parseResults); | |
| expect(edges).toHaveLength(0); | |
| }); | |
| }); | |
| describe('edge cases', () => { | |
| it('returns empty array for empty files list', () => { | |
| const edges = buildCallEdges([], new Map()); | |
| expect(edges).toEqual([]); | |
| }); | |
| it('returns empty array when imports have no names', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/utils.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './utils', names: [], resolvedPath: undefined }], | |
| })], | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'formatDate', startLine: 1, endLine: 15, params: [], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const edges = buildCallEdges(files, parseResults); | |
| expect(edges).toEqual([]); | |
| }); | |
| it('handles file with no parse result', () => { | |
| const files = [makeFile({ relativePath: 'src/app.ts' })]; | |
| const edges = buildCallEdges(files, new Map()); | |
| expect(edges).toEqual([]); | |
| }); | |
| }); | |
| }); | |
| describe('buildGraph - import and call edges integration', () => { | |
| it('includes import edges in the graph output', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/utils.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './utils', names: [], resolvedPath: undefined }], | |
| })], | |
| ['src/utils.ts', makeParseResult()], | |
| ]); | |
| const result = buildGraph(files, parseResults); | |
| const importEdges = result.edges.filter(e => e.type === 'imports'); | |
| expect(importEdges).toHaveLength(1); | |
| expect(importEdges[0]).toEqual({ | |
| source: 'src/app.ts', | |
| target: 'src/utils.ts', | |
| type: 'imports', | |
| }); | |
| }); | |
| it('includes call edges in the graph output', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/utils.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './utils', names: ['formatDate'], resolvedPath: undefined }], | |
| })], | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'formatDate', startLine: 1, endLine: 15, params: ['date'], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildGraph(files, parseResults); | |
| const callEdges = result.edges.filter(e => e.type === 'calls'); | |
| expect(callEdges).toHaveLength(1); | |
| expect(callEdges[0]).toEqual({ | |
| source: 'src/app.ts', | |
| target: 'src/utils.ts::formatDate', | |
| type: 'calls', | |
| }); | |
| }); | |
| it('combines all edge types in the output', () => { | |
| const files = [ | |
| makeFile({ relativePath: 'src/app.ts' }), | |
| makeFile({ relativePath: 'src/utils.ts' }), | |
| ]; | |
| const parseResults = new Map([ | |
| ['src/app.ts', makeParseResult({ | |
| imports: [{ source: './utils', names: ['formatDate'], resolvedPath: undefined }], | |
| functions: [ | |
| { name: 'main', startLine: 1, endLine: 20, params: [], isExported: true }, | |
| ], | |
| })], | |
| ['src/utils.ts', makeParseResult({ | |
| functions: [ | |
| { name: 'formatDate', startLine: 1, endLine: 15, params: ['date'], isExported: true }, | |
| ], | |
| })], | |
| ]); | |
| const result = buildGraph(files, parseResults); | |
| const edgeTypes = result.edges.map(e => e.type); | |
| expect(edgeTypes).toContain('contains'); | |
| expect(edgeTypes).toContain('imports'); | |
| expect(edgeTypes).toContain('calls'); | |
| }); | |
| }); | |