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 { nestjsResolver } from '../src/resolution/frameworks/nestjs'; describe('nestjsResolver.extract — HTTP', () => { it('joins @Controller prefix with @Get and links the handler', () => { const src = ` @Controller('users') export class UsersController { @Get() findAll() { return []; } } `; const { nodes, references } = nestjsResolver.extract!('users.controller.ts', src); expect(nodes).toHaveLength(1); expect(nodes[0].kind).toBe('route'); expect(nodes[0].name).toBe('GET /users'); expect(references[0].referenceName).toBe('findAll'); expect(references[0].referenceKind).toBe('references'); expect(references[0].fromNodeId).toBe(nodes[0].id); }); it('joins controller prefix with a method-level path param', () => { const src = ` @Controller('cats') export class CatsController { @Get(':id') findOne(@Param('id') id: string) { return id; } } `; const { nodes, references } = nestjsResolver.extract!('cats.controller.ts', src); expect(nodes[0].name).toBe('GET /cats/:id'); expect(references[0].referenceName).toBe('findOne'); }); it('handles an empty @Controller() and empty @Post()', () => { const src = ` @Controller() export class AppController { @Post() create() {} } `; const { nodes, references } = nestjsResolver.extract!('app.controller.ts', src); expect(nodes[0].name).toBe('POST /'); expect(references[0].referenceName).toBe('create'); }); it('covers HTTP verbs and skips intervening method decorators', () => { const src = ` @Controller('todos') export class TodosController { @Put(':id') @UseGuards(AuthGuard) update(@Param('id') id: string) {} @Delete(':id') async remove(@Param('id') id: string) {} } `; const { nodes, references } = nestjsResolver.extract!('todos.controller.ts', src); expect(nodes.map((n) => n.name)).toEqual(['PUT /todos/:id', 'DELETE /todos/:id']); expect(references.map((r) => r.referenceName)).toEqual(['update', 'remove']); }); it('attributes methods to the right controller when a file has two', () => { const src = ` @Controller('a') export class AController { @Get('x') ax() {} } @Controller('b') export class BController { @Get('y') by() {} } `; const { nodes } = nestjsResolver.extract!('multi.controller.ts', src); expect(nodes.map((n) => n.name)).toEqual(['GET /a/x', 'GET /b/y']); }); }); describe('nestjsResolver.extract — GraphQL', () => { it('emits QUERY/MUTATION nodes from a resolver, defaulting to the method name', () => { const src = ` @Resolver(() => User) export class UsersResolver { @Query(() => [User]) users() { return []; } @Mutation(() => User) createUser(@Args('input') input: CreateUserInput) {} } `; const { nodes, references } = nestjsResolver.extract!('users.resolver.ts', src); expect(nodes.map((n) => n.name)).toEqual(['QUERY users', 'MUTATION createUser']); expect(references.map((r) => r.referenceName)).toEqual(['users', 'createUser']); }); it('uses an explicit operation name when given', () => { const src = ` @Resolver() export class CatsResolver { @Query(() => Cat, { name: 'cat' }) getCat() {} } `; const { nodes } = nestjsResolver.extract!('cats.resolver.ts', src); expect(nodes[0].name).toBe('QUERY cat'); }); it('does NOT treat the REST @Query() parameter decorator as a GraphQL op', () => { const src = ` @Controller('search') export class SearchController { @Get() search(@Query() query: SearchDto) { return query; } } `; const { nodes } = nestjsResolver.extract!('search.controller.ts', src); // Only the HTTP route — the @Query() param decorator must be ignored. expect(nodes.map((n) => n.name)).toEqual(['GET /search']); }); }); describe('nestjsResolver.extract — microservices & websockets', () => { it('extracts @MessagePattern and @EventPattern handlers', () => { const src = ` @Controller() export class MathController { @MessagePattern({ cmd: 'sum' }) accumulate(data: number[]) {} @EventPattern('user.created') handleUserCreated(data: any) {} } `; const { nodes, references } = nestjsResolver.extract!('math.controller.ts', src); expect(nodes.map((n) => n.name)).toEqual(['MESSAGE sum', 'EVENT user.created']); expect(references.map((r) => r.referenceName)).toEqual(['accumulate', 'handleUserCreated']); }); it('extracts @SubscribeMessage handlers with the gateway namespace', () => { const src = ` @WebSocketGateway({ namespace: 'chat' }) export class ChatGateway { @SubscribeMessage('message') handleMessage(@MessageBody() data: string) {} } `; const { nodes, references } = nestjsResolver.extract!('chat.gateway.ts', src); expect(nodes[0].name).toBe('WS chat:message'); expect(references[0].referenceName).toBe('handleMessage'); }); it('extracts @SubscribeMessage without a namespace', () => { const src = ` @WebSocketGateway() export class EventsGateway { @SubscribeMessage('events') onEvent() {} } `; const { nodes } = nestjsResolver.extract!('events.gateway.ts', src); expect(nodes[0].name).toBe('WS events'); }); it('returns empty for a non-JS/TS file', () => { const { nodes, references } = nestjsResolver.extract!('thing.py', '@Controller("x")'); expect(nodes).toEqual([]); expect(references).toEqual([]); }); }); describe('nestjsResolver.detect', () => { const baseContext = { getNodesInFile: () => [], getNodesByName: () => [], getNodesByQualifiedName: () => [], getNodesByKind: () => [], fileExists: () => false, getProjectRoot: () => '/test', getAllFiles: () => [], getNodesByLowerName: () => [], getImportMappings: () => [], }; it('detects @nestjs/* in package.json', () => { const context = { ...baseContext, readFile: (p: string) => p === 'package.json' ? JSON.stringify({ dependencies: { '@nestjs/common': '^10.0.0' } }) : null, }; expect(nestjsResolver.detect(context as any)).toBe(true); }); it('detects @Controller in a *.controller.ts file when package.json is absent', () => { const context = { ...baseContext, getAllFiles: () => ['src/users.controller.ts'], readFile: (p: string) => p === 'src/users.controller.ts' ? `@Controller('users')\nexport class UsersController {}` : null, }; expect(nestjsResolver.detect(context as any)).toBe(true); }); it('returns false for a non-Nest project', () => { const context = { ...baseContext, readFile: (p: string) => p === 'package.json' ? JSON.stringify({ dependencies: { express: '^4' } }) : null, }; expect(nestjsResolver.detect(context as any)).toBe(false); }); }); describe('nestjsResolver.resolve', () => { const baseContext = { getNodesInFile: () => [], getNodesByName: () => [], getNodesByQualifiedName: () => [], getNodesByKind: () => [], fileExists: () => false, readFile: () => null, getProjectRoot: () => '/test', getAllFiles: () => [], getNodesByLowerName: () => [], getImportMappings: () => [], }; it('resolves an injected *Service reference to the class in a *.service.ts file', () => { const svcNode: Node = { id: 'class:src/users/users.service.ts:UsersService:3', kind: 'class', name: 'UsersService', qualifiedName: 'src/users/users.service.ts::UsersService', filePath: 'src/users/users.service.ts', language: 'typescript', startLine: 3, endLine: 3, startColumn: 0, endColumn: 0, updatedAt: Date.now(), }; const context = { ...baseContext, getNodesByName: (n: string) => (n === 'UsersService' ? [svcNode] : []), }; const ref = { fromNodeId: 'class:src/users/users.controller.ts:UsersController:5', referenceName: 'UsersService', referenceKind: 'references' as const, line: 6, column: 4, filePath: 'src/users/users.controller.ts', language: 'typescript' as const, }; const result = nestjsResolver.resolve(ref, context as any); expect(result?.targetNodeId).toBe(svcNode.id); expect(result?.resolvedBy).toBe('framework'); expect(result?.confidence).toBeGreaterThanOrEqual(0.85); }); it('returns null for a name without a provider suffix', () => { const ref = { fromNodeId: 'x', referenceName: 'doThing', referenceKind: 'references' as const, line: 1, column: 1, filePath: 'a.ts', language: 'typescript' as const, }; expect(nestjsResolver.resolve(ref, baseContext as any)).toBeNull(); }); }); 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']); }); it('nestjs: skips // and /* */ commented decorators', () => { const src = ` @Controller('users') export class UsersController { // @Get('fake') // fake() {} /* @Post('also-fake') alsoFake() {} */ @Get('real') real() {} } `; const { nodes, references } = nestjsResolver.extract!('users.controller.ts', src); expect(nodes.map((n) => n.name)).toEqual(['GET /users/real']); expect(references.map((r) => r.referenceName)).toEqual(['real']); }); });