import { describe, it, expect } from 'vitest'; import type { FrameworkResolver, UnresolvedRef } from '../src/resolution/types'; import type { Node } from '../src/types'; describe('FrameworkResolver.extract interface', () => { it('extract() returns { nodes, references }', () => { const resolver: FrameworkResolver = { name: 'fake', detect: () => true, resolve: () => null, languages: ['python'], extract: (_filePath: string, _content: string) => ({ nodes: [] as Node[], references: [] as UnresolvedRef[], }), }; const result = resolver.extract!('foo.py', ''); expect(result).toEqual({ nodes: [], references: [] }); }); }); import { getApplicableFrameworks } from '../src/resolution/frameworks'; import type { FrameworkResolver } from '../src/resolution/types'; describe('getApplicableFrameworks', () => { const pyFw: FrameworkResolver = { name: 'py', languages: ['python'], detect: () => true, resolve: () => null }; const jsFw: FrameworkResolver = { name: 'js', languages: ['javascript', 'typescript'], detect: () => true, resolve: () => null }; const anyFw: FrameworkResolver = { name: 'any', detect: () => true, resolve: () => null }; it('filters by language', () => { const result = getApplicableFrameworks([pyFw, jsFw, anyFw], 'python'); expect(result.map(r => r.name)).toEqual(['py', 'any']); }); it('returns anyFw-only when language has no matches', () => { const result = getApplicableFrameworks([pyFw, jsFw, anyFw], 'rust'); expect(result.map(r => r.name)).toEqual(['any']); }); }); import { djangoResolver } from '../src/resolution/frameworks/python'; describe('djangoResolver.extract', () => { it('extracts route node and reference for path() with CBV.as_view()', () => { const src = ` from django.urls import path from users.views import UserListView urlpatterns = [ path('users/', UserListView.as_view(), name='user-list'), ] `; const { nodes, references } = djangoResolver.extract!('users/urls.py', src); expect(nodes).toHaveLength(1); expect(nodes[0].kind).toBe('route'); expect(nodes[0].name).toBe('users/'); expect(references).toHaveLength(1); expect(references[0].referenceName).toBe('UserListView'); expect(references[0].referenceKind).toBe('references'); expect(references[0].fromNodeId).toBe(nodes[0].id); }); it('extracts route for path() with dotted module.Class.as_view()', () => { const src = `from django.urls import path\nfrom api.v1 import views as api_v1_views\nurlpatterns = [path('api/', api_v1_views.UserListView.as_view())]\n`; const { nodes, references } = djangoResolver.extract!('api/urls.py', src); expect(nodes).toHaveLength(1); expect(references[0].referenceName).toBe('UserListView'); }); it('extracts route for path() with bare function view', () => { const src = `from django.urls import path\nurlpatterns = [path('home/', home_view, name='home')]\n`; const { nodes, references } = djangoResolver.extract!('home/urls.py', src); expect(references[0].referenceName).toBe('home_view'); }); it('extracts route for path() with include()', () => { const src = `from django.urls import path, include\nurlpatterns = [path('api/', include('api.urls'))]\n`; const { nodes, references } = djangoResolver.extract!('root/urls.py', src); expect(nodes).toHaveLength(1); expect(nodes[0].kind).toBe('route'); expect(references[0].referenceName).toBe('api.urls'); expect(references[0].referenceKind).toBe('imports'); }); it('extracts routes for re_path and url', () => { const src = `from django.urls import re_path, url\nurlpatterns = [re_path(r'^users/$', UserView), url(r'^old/$', OldView)]\n`; const { nodes } = djangoResolver.extract!('legacy/urls.py', src); expect(nodes).toHaveLength(2); expect(nodes.map(n => n.name)).toEqual(['^users/$', '^old/$']); }); it('returns empty result for a non-urls.py python file', () => { const src = `def foo(): return 1\n`; const { nodes, references } = djangoResolver.extract!('views.py', src); expect(nodes).toEqual([]); expect(references).toEqual([]); }); }); import { flaskResolver, fastapiResolver } from '../src/resolution/frameworks/python'; describe('flaskResolver.extract', () => { it('extracts route and reference from @app.route', () => { const src = ` @app.route('/users') def list_users(): return [] `; const { nodes, references } = flaskResolver.extract!('app.py', src); expect(nodes).toHaveLength(1); expect(nodes[0].kind).toBe('route'); expect(nodes[0].name).toBe('GET /users'); expect(references[0].referenceName).toBe('list_users'); }); it('extracts blueprint routes', () => { const src = ` @users_bp.route('/', methods=['POST']) def create_user(id): pass `; const { nodes, references } = flaskResolver.extract!('routes.py', src); expect(nodes[0].name).toBe('POST /'); expect(references[0].referenceName).toBe('create_user'); }); }); describe('fastapiResolver.extract', () => { it('extracts route and reference from @app.get', () => { const src = ` @app.get('/users') async def list_users(): return [] `; const { nodes, references } = fastapiResolver.extract!('main.py', src); expect(nodes[0].name).toBe('GET /users'); expect(references[0].referenceName).toBe('list_users'); }); it('extracts route from router.post', () => { const src = ` @router.post('/items') def create_item(item: Item): pass `; const { nodes, references } = fastapiResolver.extract!('items.py', src); expect(nodes[0].name).toBe('POST /items'); expect(references[0].referenceName).toBe('create_item'); }); }); import { expressResolver } from '../src/resolution/frameworks/express'; describe('expressResolver.extract', () => { it('extracts route with inline handler reference', () => { const src = `app.get('/users', listUsers);\n`; const { nodes, references } = expressResolver.extract!('routes.ts', src); expect(nodes).toHaveLength(1); expect(nodes[0].name).toBe('GET /users'); expect(references[0].referenceName).toBe('listUsers'); }); it('extracts route with router.post and middleware chain', () => { const src = `router.post('/items', auth, createItem);\n`; const { nodes, references } = expressResolver.extract!('items.ts', src); expect(nodes[0].name).toBe('POST /items'); // Multiple handlers: prefer the LAST one (convention: middleware first, handler last) expect(references[0].referenceName).toBe('createItem'); }); it('extracts route with controller method reference', () => { const src = `app.get('/x', userController.list);\n`; const { nodes, references } = expressResolver.extract!('routes.ts', src); expect(references[0].referenceName).toBe('list'); }); }); import { laravelResolver } from '../src/resolution/frameworks/laravel'; describe('laravelResolver.extract', () => { it('extracts route with controller tuple syntax', () => { const src = `Route::get('/users', [UserController::class, 'index']);\n`; const { nodes, references } = laravelResolver.extract!('routes/web.php', src); expect(nodes[0].name).toBe('GET /users'); expect(references[0].referenceName).toBe('index'); }); it('extracts route with Controller@action syntax', () => { const src = `Route::post('/users', 'UserController@store');\n`; const { nodes, references } = laravelResolver.extract!('routes/web.php', src); expect(references[0].referenceName).toBe('store'); }); it('extracts resource route', () => { const src = `Route::resource('users', UserController::class);\n`; const { nodes, references } = laravelResolver.extract!('routes/web.php', src); expect(nodes[0].kind).toBe('route'); expect(references[0].referenceName).toBe('UserController'); }); }); import { railsResolver } from '../src/resolution/frameworks/ruby'; describe('railsResolver.extract', () => { it('extracts route with controller#action syntax', () => { const src = `get '/users', to: 'users#index'\n`; const { nodes, references } = railsResolver.extract!('config/routes.rb', src); expect(nodes[0].name).toBe('GET /users'); expect(references[0].referenceName).toBe('index'); }); it('extracts route without to: keyword', () => { const src = `post '/items' => 'items#create'\n`; const { nodes, references } = railsResolver.extract!('config/routes.rb', src); expect(references[0].referenceName).toBe('create'); }); }); import { springResolver } from '../src/resolution/frameworks/java'; describe('springResolver.extract', () => { it('extracts route with @GetMapping and next method', () => { const src = ` @GetMapping("/users") public List listUsers() { return users; } `; const { nodes, references } = springResolver.extract!('UserController.java', src); expect(nodes[0].name).toBe('GET /users'); expect(references[0].referenceName).toBe('listUsers'); }); }); import { goResolver } from '../src/resolution/frameworks/go'; describe('goResolver.extract', () => { it('extracts route from r.GET', () => { const src = `r.GET("/users", listUsers)\n`; const { nodes, references } = goResolver.extract!('main.go', src); expect(nodes[0].name).toBe('GET /users'); expect(references[0].referenceName).toBe('listUsers'); }); it('extracts route from router.HandleFunc', () => { const src = `router.HandleFunc("/items", createItem)\n`; const { nodes, references } = goResolver.extract!('main.go', src); expect(references[0].referenceName).toBe('createItem'); }); }); import { rustResolver } from '../src/resolution/frameworks/rust'; describe('rustResolver.extract', () => { it('extracts route from axum .route with get()', () => { const src = `let app = Router::new().route("/users", get(list_users));\n`; const { nodes, references } = rustResolver.extract!('main.rs', src); expect(nodes[0].name).toBe('GET /users'); expect(references[0].referenceName).toBe('list_users'); }); }); describe('rustResolver.resolve cargo workspace crates', () => { it('resolves crate name from workspace member lib.rs', () => { const workspaceCargo = ` [workspace] members = ["crates/mytool-core", "crates/mytool-fetcher"] `; const coreCargo = ` [package] name = "mytool-core" version = "0.1.0" `; const libNode: Node = { id: 'module:crates/mytool-core/src/lib.rs:mytool_core:1', kind: 'module', name: 'mytool_core', qualifiedName: 'crates/mytool-core/src/lib.rs::mytool_core', filePath: 'crates/mytool-core/src/lib.rs', language: 'rust', startLine: 1, endLine: 1, startColumn: 0, endColumn: 0, updatedAt: Date.now(), }; const context = { getNodesInFile: (fp: string) => (fp === 'crates/mytool-core/src/lib.rs' ? [libNode] : []), getNodesByName: () => [], getNodesByQualifiedName: () => [], getNodesByKind: () => [], fileExists: (p: string) => ( p === 'Cargo.toml' || p === 'crates/mytool-core/Cargo.toml' || p === 'crates/mytool-core/src/lib.rs' ), readFile: (p: string) => { if (p === 'Cargo.toml') return workspaceCargo; if (p === 'crates/mytool-core/Cargo.toml') return coreCargo; return null; }, getProjectRoot: () => '/test', getAllFiles: () => [ 'Cargo.toml', 'crates/mytool-core/Cargo.toml', 'crates/mytool-core/src/lib.rs', ], getNodesByLowerName: () => [], getImportMappings: () => [], }; const ref = { fromNodeId: 'fn:crates/mytool-fetcher/src/main.rs:main:1', referenceName: 'mytool_core', referenceKind: 'references' as const, line: 1, column: 1, filePath: 'crates/mytool-fetcher/src/main.rs', language: 'rust' as const, }; const result = rustResolver.resolve(ref, context); expect(result?.targetNodeId).toBe(libNode.id); expect(result?.resolvedBy).toBe('framework'); // Workspace-manifest hits are unambiguous and must beat name-matcher's // self-file matches (0.7) so cross-crate `imports` edges materialize. expect(result?.confidence).toBeGreaterThanOrEqual(0.9); }); it('resolves crate name from workspace member main.rs when lib.rs is absent', () => { const workspaceCargo = ` [workspace] members = [ "crates/mytool-runner", ] `; const runnerCargo = ` [package] name = "mytool-runner" version = "0.1.0" `; const mainNode: Node = { id: 'module:crates/mytool-runner/src/main.rs:mytool_runner:1', kind: 'module', name: 'mytool_runner', qualifiedName: 'crates/mytool-runner/src/main.rs::mytool_runner', filePath: 'crates/mytool-runner/src/main.rs', language: 'rust', startLine: 1, endLine: 1, startColumn: 0, endColumn: 0, updatedAt: Date.now(), }; const context = { getNodesInFile: (fp: string) => (fp === 'crates/mytool-runner/src/main.rs' ? [mainNode] : []), getNodesByName: () => [], getNodesByQualifiedName: () => [], getNodesByKind: () => [], fileExists: (p: string) => ( p === 'Cargo.toml' || p === 'crates/mytool-runner/Cargo.toml' || p === 'crates/mytool-runner/src/main.rs' ), readFile: (p: string) => { if (p === 'Cargo.toml') return workspaceCargo; if (p === 'crates/mytool-runner/Cargo.toml') return runnerCargo; return null; }, getProjectRoot: () => '/test', getAllFiles: () => [ 'Cargo.toml', 'crates/mytool-runner/Cargo.toml', 'crates/mytool-runner/src/main.rs', ], getNodesByLowerName: () => [], getImportMappings: () => [], }; const ref = { fromNodeId: 'fn:crates/mytool-runner/src/main.rs:main:1', referenceName: 'mytool_runner', referenceKind: 'references' as const, line: 1, column: 1, filePath: 'crates/mytool-runner/src/main.rs', language: 'rust' as const, }; const result = rustResolver.resolve(ref, context); expect(result?.targetNodeId).toBe(mainNode.id); expect(result?.resolvedBy).toBe('framework'); }); it('resolves crate name when members uses a glob (crates/*)', () => { const workspaceCargo = ` [workspace] members = ["crates/*"] `; const fooCargo = ` [package] name = "mytool-foo" version = "0.1.0" `; const barCargo = ` [package] name = "mytool-bar" version = "0.1.0" `; const fooLib: Node = { id: 'module:crates/mytool-foo/src/lib.rs:mytool_foo:1', kind: 'module', name: 'mytool_foo', qualifiedName: 'crates/mytool-foo/src/lib.rs::mytool_foo', filePath: 'crates/mytool-foo/src/lib.rs', language: 'rust', startLine: 1, endLine: 1, startColumn: 0, endColumn: 0, updatedAt: Date.now(), }; const barLib: Node = { id: 'module:crates/mytool-bar/src/lib.rs:mytool_bar:1', kind: 'module', name: 'mytool_bar', qualifiedName: 'crates/mytool-bar/src/lib.rs::mytool_bar', filePath: 'crates/mytool-bar/src/lib.rs', language: 'rust', startLine: 1, endLine: 1, startColumn: 0, endColumn: 0, updatedAt: Date.now(), }; const filesByPath: Record = { 'Cargo.toml': workspaceCargo, 'crates/mytool-foo/Cargo.toml': fooCargo, 'crates/mytool-bar/Cargo.toml': barCargo, }; const nodesByFile: Record = { 'crates/mytool-foo/src/lib.rs': [fooLib], 'crates/mytool-bar/src/lib.rs': [barLib], }; const dirsByPath: Record = { '.': ['crates'], crates: ['mytool-foo', 'mytool-bar'], 'crates/mytool-foo': ['src'], 'crates/mytool-bar': ['src'], }; const context = { getNodesInFile: (fp: string) => nodesByFile[fp] ?? [], getNodesByName: () => [], getNodesByQualifiedName: () => [], getNodesByKind: () => [], fileExists: (p: string) => ( Object.prototype.hasOwnProperty.call(filesByPath, p) || Object.prototype.hasOwnProperty.call(nodesByFile, p) ), readFile: (p: string) => filesByPath[p] ?? null, getProjectRoot: () => '/test', getAllFiles: () => [ 'Cargo.toml', ...Object.keys(filesByPath).filter((p) => p !== 'Cargo.toml'), ...Object.keys(nodesByFile), ], getNodesByLowerName: () => [], getImportMappings: () => [], listDirectories: (rel: string) => dirsByPath[rel] ?? [], }; const fooRef = { fromNodeId: 'fn:crates/mytool-bar/src/lib.rs:other:1', referenceName: 'mytool_foo', referenceKind: 'references' as const, line: 1, column: 1, filePath: 'crates/mytool-bar/src/lib.rs', language: 'rust' as const, }; const barRef = { fromNodeId: 'fn:crates/mytool-foo/src/lib.rs:other:1', referenceName: 'mytool_bar', referenceKind: 'references' as const, line: 1, column: 1, filePath: 'crates/mytool-foo/src/lib.rs', language: 'rust' as const, }; expect(rustResolver.resolve(fooRef, context)?.targetNodeId).toBe(fooLib.id); expect(rustResolver.resolve(barRef, context)?.targetNodeId).toBe(barLib.id); }); it('resolves crate name when members uses a name glob at root (helix-*)', () => { const workspaceCargo = ` [workspace] members = ["helix-*"] `; const coreCargo = ` [package] name = "helix-core" version = "0.1.0" `; const coreLib: Node = { id: 'module:helix-core/src/lib.rs:helix_core:1', kind: 'module', name: 'helix_core', qualifiedName: 'helix-core/src/lib.rs::helix_core', filePath: 'helix-core/src/lib.rs', language: 'rust', startLine: 1, endLine: 1, startColumn: 0, endColumn: 0, updatedAt: Date.now(), }; const filesByPath: Record = { 'Cargo.toml': workspaceCargo, 'helix-core/Cargo.toml': coreCargo, }; const nodesByFile: Record = { 'helix-core/src/lib.rs': [coreLib], }; const dirsByPath: Record = { '.': ['helix-core', 'docs', 'target'], 'helix-core': ['src'], }; const context = { getNodesInFile: (fp: string) => nodesByFile[fp] ?? [], getNodesByName: () => [], getNodesByQualifiedName: () => [], getNodesByKind: () => [], fileExists: (p: string) => ( Object.prototype.hasOwnProperty.call(filesByPath, p) || Object.prototype.hasOwnProperty.call(nodesByFile, p) ), readFile: (p: string) => filesByPath[p] ?? null, getProjectRoot: () => '/test', getAllFiles: () => [ 'Cargo.toml', ...Object.keys(filesByPath).filter((p) => p !== 'Cargo.toml'), ...Object.keys(nodesByFile), ], getNodesByLowerName: () => [], getImportMappings: () => [], listDirectories: (rel: string) => dirsByPath[rel] ?? [], }; const ref = { fromNodeId: 'fn:helix-core/src/lib.rs:other:1', referenceName: 'helix_core', referenceKind: 'references' as const, line: 1, column: 1, filePath: 'helix-core/src/lib.rs', language: 'rust' as const, }; expect(rustResolver.resolve(ref, context)?.targetNodeId).toBe(coreLib.id); }); }); import { aspnetResolver } from '../src/resolution/frameworks/csharp'; describe('aspnetResolver.extract', () => { it('extracts route from [HttpGet] attribute', () => { const src = ` [HttpGet("/users")] public IActionResult ListUsers() { return Ok(); } `; const { nodes, references } = aspnetResolver.extract!('UserController.cs', src); expect(nodes[0].name).toBe('GET /users'); expect(references[0].referenceName).toBe('ListUsers'); }); }); import { vaporResolver } from '../src/resolution/frameworks/swift'; describe('vaporResolver.extract', () => { it('extracts route from app.get with use:', () => { const src = `app.get("users", use: listUsers)\n`; const { nodes, references } = vaporResolver.extract!('routes.swift', src); expect(nodes[0].name).toBe('GET users'); expect(references[0].referenceName).toBe('listUsers'); }); }); import { reactResolver } from '../src/resolution/frameworks/react'; import { svelteResolver } from '../src/resolution/frameworks/svelte'; describe('reactResolver.extract (smoke)', () => { it('returns { nodes, references } shape', () => { const src = `}/>`; const result = reactResolver.extract!('App.tsx', src); expect(result).toHaveProperty('nodes'); expect(result).toHaveProperty('references'); expect(Array.isArray(result.nodes)).toBe(true); expect(Array.isArray(result.references)).toBe(true); }); }); describe('svelteResolver.extract (smoke)', () => { it('returns { nodes, references } shape', () => { const result = svelteResolver.extract!('+page.svelte', ''); expect(result).toHaveProperty('nodes'); expect(result).toHaveProperty('references'); }); }); // Regression tests: commented-out and docstring route examples must NOT // surface as phantom route nodes. These would have failed before the // strip-comments wiring (the regex would happily scan comments/docstrings). describe('framework extractors ignore commented-out routes', () => { it('django: skips line-comment and docstring routes', () => { const src = ` # urls.py example: # path('/admin/', AdminPanel.as_view()) """ Other routing example: path('/users/', UserListView.as_view()) """ urlpatterns = [path('/real/', RealView.as_view())] `; const result = djangoResolver.extract!('app/urls.py', src); const urls = result.nodes.map((n) => n.name); expect(urls).toEqual(['/real/']); }); it('flask: skips commented-out @app.route', () => { const src = ` # @app.route('/fake') # def fake_view(): # return '' @app.route('/real') def real_view(): return '' `; const { nodes, references } = flaskResolver.extract!('app.py', src); expect(nodes.map((n) => n.name)).toEqual(['GET /real']); expect(references.map((r) => r.referenceName)).toEqual(['real_view']); }); it('fastapi: skips docstring example routes', () => { const src = ` """ Example: @app.get('/in-docstring') async def doc(): pass """ @app.get('/real') async def real_handler(): return {} `; const { nodes, references } = fastapiResolver.extract!('main.py', src); expect(nodes.map((n) => n.name)).toEqual(['GET /real']); expect(references.map((r) => r.referenceName)).toEqual(['real_handler']); }); it('express: skips // and /* */ commented routes', () => { const src = ` // app.get('/fake', fakeHandler); /* router.post('/also-fake', otherHandler); */ app.get('/real', realHandler); `; const { nodes, references } = expressResolver.extract!('routes.ts', src); expect(nodes.map((n) => n.name)).toEqual(['GET /real']); expect(references.map((r) => r.referenceName)).toEqual(['realHandler']); }); it('laravel: skips // # and /* */ commented Route::* calls', () => { const src = ` n.name)).toEqual(['GET /real']); expect(references.map((r) => r.referenceName)).toEqual(['index']); }); it('rails: skips =begin/=end and # commented routes', () => { const src = ` # get '/fake', to: 'fake#index' =begin get '/also-fake', to: 'fake#show' =end get '/real', to: 'real#index' `; const { nodes, references } = railsResolver.extract!('config/routes.rb', src); expect(nodes.map((n) => n.name)).toEqual(['GET /real']); expect(references.map((r) => r.referenceName)).toEqual(['index']); }); it('spring: skips // and /* */ commented @GetMapping', () => { const src = ` // @GetMapping("/fake") // public List fake() { return null; } /* @PostMapping("/also-fake") public void alsoFake() {} */ @GetMapping("/real") public List listUsers() { return users; } `; const { nodes, references } = springResolver.extract!('UserController.java', src); expect(nodes.map((n) => n.name)).toEqual(['GET /real']); expect(references.map((r) => r.referenceName)).toEqual(['listUsers']); }); it('go: skips // and /* */ commented router.METHOD calls', () => { const src = ` // r.GET("/fake", fakeHandler) /* r.POST("/also-fake", anotherHandler) */ r.GET("/real", listUsers) `; const { nodes, references } = goResolver.extract!('main.go', src); expect(nodes.map((n) => n.name)).toEqual(['GET /real']); expect(references.map((r) => r.referenceName)).toEqual(['listUsers']); }); it('rust: skips // and nested /* */ commented .route() calls', () => { const src = ` // .route("/fake", get(fake_handler)) /* outer /* inner .route("/inner-fake", get(x)) */ still .route("/outer-fake", get(y)) */ let app = Router::new().route("/real", get(list_users)); `; const { nodes, references } = rustResolver.extract!('main.rs', src); expect(nodes.map((n) => n.name)).toEqual(['GET /real']); expect(references.map((r) => r.referenceName)).toEqual(['list_users']); }); it('aspnet: skips // and /* */ commented [HttpGet] attributes', () => { const src = ` // [HttpGet("/fake")] // public IActionResult Fake() { return Ok(); } /* [HttpPost("/also-fake")] public IActionResult AlsoFake() { return Ok(); } */ [HttpGet("/real")] public IActionResult ListUsers() { return Ok(); } `; const { nodes, references } = aspnetResolver.extract!('UserController.cs', src); expect(nodes.map((n) => n.name)).toEqual(['GET /real']); expect(references.map((r) => r.referenceName)).toEqual(['ListUsers']); }); it('vapor: skips // and /* */ commented app.METHOD calls', () => { const src = ` // app.get("fake", use: fakeHandler) /* app.post("also-fake", use: anotherHandler) */ app.get("real", use: listUsers) `; const { nodes, references } = vaporResolver.extract!('routes.swift', src); expect(nodes.map((n) => n.name)).toEqual(['GET real']); expect(references.map((r) => r.referenceName)).toEqual(['listUsers']); }); });