frameworks.test.ts 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215
  1. import { describe, it, expect } from 'vitest';
  2. import type { FrameworkResolver, UnresolvedRef } from '../src/resolution/types';
  3. import type { Node } from '../src/types';
  4. describe('FrameworkResolver.extract interface', () => {
  5. it('extract() returns { nodes, references }', () => {
  6. const resolver: FrameworkResolver = {
  7. name: 'fake',
  8. detect: () => true,
  9. resolve: () => null,
  10. languages: ['python'],
  11. extract: (_filePath: string, _content: string) => ({
  12. nodes: [] as Node[],
  13. references: [] as UnresolvedRef[],
  14. }),
  15. };
  16. const result = resolver.extract!('foo.py', '');
  17. expect(result).toEqual({ nodes: [], references: [] });
  18. });
  19. });
  20. import { getApplicableFrameworks } from '../src/resolution/frameworks';
  21. import type { FrameworkResolver } from '../src/resolution/types';
  22. describe('getApplicableFrameworks', () => {
  23. const pyFw: FrameworkResolver = { name: 'py', languages: ['python'], detect: () => true, resolve: () => null };
  24. const jsFw: FrameworkResolver = { name: 'js', languages: ['javascript', 'typescript'], detect: () => true, resolve: () => null };
  25. const anyFw: FrameworkResolver = { name: 'any', detect: () => true, resolve: () => null };
  26. it('filters by language', () => {
  27. const result = getApplicableFrameworks([pyFw, jsFw, anyFw], 'python');
  28. expect(result.map(r => r.name)).toEqual(['py', 'any']);
  29. });
  30. it('returns anyFw-only when language has no matches', () => {
  31. const result = getApplicableFrameworks([pyFw, jsFw, anyFw], 'rust');
  32. expect(result.map(r => r.name)).toEqual(['any']);
  33. });
  34. });
  35. import { djangoResolver } from '../src/resolution/frameworks/python';
  36. describe('djangoResolver.extract', () => {
  37. it('extracts route node and reference for path() with CBV.as_view()', () => {
  38. const src = `
  39. from django.urls import path
  40. from users.views import UserListView
  41. urlpatterns = [
  42. path('users/', UserListView.as_view(), name='user-list'),
  43. ]
  44. `;
  45. const { nodes, references } = djangoResolver.extract!('users/urls.py', src);
  46. expect(nodes).toHaveLength(1);
  47. expect(nodes[0].kind).toBe('route');
  48. expect(nodes[0].name).toBe('users/');
  49. expect(references).toHaveLength(1);
  50. expect(references[0].referenceName).toBe('UserListView');
  51. expect(references[0].referenceKind).toBe('references');
  52. expect(references[0].fromNodeId).toBe(nodes[0].id);
  53. });
  54. it('extracts route for path() with dotted module.Class.as_view()', () => {
  55. 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`;
  56. const { nodes, references } = djangoResolver.extract!('api/urls.py', src);
  57. expect(nodes).toHaveLength(1);
  58. expect(references[0].referenceName).toBe('UserListView');
  59. });
  60. it('extracts route for path() with bare function view', () => {
  61. const src = `from django.urls import path\nurlpatterns = [path('home/', home_view, name='home')]\n`;
  62. const { nodes, references } = djangoResolver.extract!('home/urls.py', src);
  63. expect(references[0].referenceName).toBe('home_view');
  64. });
  65. it('extracts route for path() with include()', () => {
  66. const src = `from django.urls import path, include\nurlpatterns = [path('api/', include('api.urls'))]\n`;
  67. const { nodes, references } = djangoResolver.extract!('root/urls.py', src);
  68. expect(nodes).toHaveLength(1);
  69. expect(nodes[0].kind).toBe('route');
  70. expect(references[0].referenceName).toBe('api.urls');
  71. expect(references[0].referenceKind).toBe('imports');
  72. });
  73. it('extracts routes for re_path and url', () => {
  74. const src = `from django.urls import re_path, url\nurlpatterns = [re_path(r'^users/$', UserView), url(r'^old/$', OldView)]\n`;
  75. const { nodes } = djangoResolver.extract!('legacy/urls.py', src);
  76. expect(nodes).toHaveLength(2);
  77. expect(nodes.map(n => n.name)).toEqual(['^users/$', '^old/$']);
  78. });
  79. it('returns empty result for a non-urls.py python file', () => {
  80. const src = `def foo(): return 1\n`;
  81. const { nodes, references } = djangoResolver.extract!('views.py', src);
  82. expect(nodes).toEqual([]);
  83. expect(references).toEqual([]);
  84. });
  85. });
  86. import { flaskResolver, fastapiResolver } from '../src/resolution/frameworks/python';
  87. describe('flaskResolver.extract', () => {
  88. it('extracts route and reference from @app.route', () => {
  89. const src = `
  90. @app.route('/users')
  91. def list_users():
  92. return []
  93. `;
  94. const { nodes, references } = flaskResolver.extract!('app.py', src);
  95. expect(nodes).toHaveLength(1);
  96. expect(nodes[0].kind).toBe('route');
  97. expect(nodes[0].name).toBe('GET /users');
  98. expect(references[0].referenceName).toBe('list_users');
  99. });
  100. it('extracts blueprint routes', () => {
  101. const src = `
  102. @users_bp.route('/<id>', methods=['POST'])
  103. def create_user(id):
  104. pass
  105. `;
  106. const { nodes, references } = flaskResolver.extract!('routes.py', src);
  107. expect(nodes[0].name).toBe('POST /<id>');
  108. expect(references[0].referenceName).toBe('create_user');
  109. });
  110. it('resolves the handler across an intervening decorator (@login_required)', () => {
  111. const src = `
  112. @bp.route('/profile')
  113. @login_required
  114. def profile():
  115. return render_template('profile.html')
  116. `;
  117. const { nodes, references } = flaskResolver.extract!('routes.py', src);
  118. expect(nodes[0].name).toBe('GET /profile');
  119. expect(references[0].referenceName).toBe('profile');
  120. });
  121. it('extracts stacked @x.route decorators bound to one view', () => {
  122. const src = `
  123. @bp.route('/', methods=['GET', 'POST'])
  124. @bp.route('/index', methods=['GET', 'POST'])
  125. @login_required
  126. def index():
  127. return render_template('index.html')
  128. `;
  129. const { nodes, references } = flaskResolver.extract!('routes.py', src);
  130. expect(nodes.map((n) => n.name)).toEqual(['GET /', 'GET /index']);
  131. expect(references.map((r) => r.referenceName)).toEqual(['index', 'index']);
  132. });
  133. });
  134. describe('fastapiResolver.extract', () => {
  135. it('extracts route and reference from @app.get', () => {
  136. const src = `
  137. @app.get('/users')
  138. async def list_users():
  139. return []
  140. `;
  141. const { nodes, references } = fastapiResolver.extract!('main.py', src);
  142. expect(nodes[0].name).toBe('GET /users');
  143. expect(references[0].referenceName).toBe('list_users');
  144. });
  145. it('extracts route from router.post', () => {
  146. const src = `
  147. @router.post('/items')
  148. def create_item(item: Item):
  149. pass
  150. `;
  151. const { nodes, references } = fastapiResolver.extract!('items.py', src);
  152. expect(nodes[0].name).toBe('POST /items');
  153. expect(references[0].referenceName).toBe('create_item');
  154. });
  155. it('extracts a route mounted at the router/prefix root (empty path)', () => {
  156. const src = `
  157. @router.get("", response_model=ListOfArticles, name="articles:list")
  158. async def list_articles():
  159. return []
  160. `;
  161. const { nodes, references } = fastapiResolver.extract!('articles.py', src);
  162. expect(nodes[0].name).toBe('GET /');
  163. expect(references[0].referenceName).toBe('list_articles');
  164. });
  165. it('extracts a multi-line decorator with an empty path', () => {
  166. const src = `
  167. @router.post(
  168. "",
  169. status_code=201,
  170. response_model=ArticleInResponse,
  171. )
  172. async def create_article():
  173. pass
  174. `;
  175. const { nodes, references } = fastapiResolver.extract!('articles.py', src);
  176. expect(nodes[0].name).toBe('POST /');
  177. expect(references[0].referenceName).toBe('create_article');
  178. });
  179. });
  180. import { expressResolver } from '../src/resolution/frameworks/express';
  181. describe('expressResolver.extract', () => {
  182. it('extracts route with inline handler reference', () => {
  183. const src = `app.get('/users', listUsers);\n`;
  184. const { nodes, references } = expressResolver.extract!('routes.ts', src);
  185. expect(nodes).toHaveLength(1);
  186. expect(nodes[0].name).toBe('GET /users');
  187. expect(references[0].referenceName).toBe('listUsers');
  188. });
  189. it('extracts route with router.post and middleware chain', () => {
  190. const src = `router.post('/items', auth, createItem);\n`;
  191. const { nodes, references } = expressResolver.extract!('items.ts', src);
  192. expect(nodes[0].name).toBe('POST /items');
  193. // Multiple handlers: prefer the LAST one (convention: middleware first, handler last)
  194. expect(references[0].referenceName).toBe('createItem');
  195. });
  196. it('extracts route with controller method reference', () => {
  197. const src = `app.get('/x', userController.list);\n`;
  198. const { nodes, references } = expressResolver.extract!('routes.ts', src);
  199. expect(references[0].referenceName).toBe('list');
  200. });
  201. });
  202. import { nestjsResolver } from '../src/resolution/frameworks/nestjs';
  203. describe('nestjsResolver.extract — HTTP', () => {
  204. it('joins @Controller prefix with @Get and links the handler', () => {
  205. const src = `
  206. @Controller('users')
  207. export class UsersController {
  208. @Get()
  209. findAll() { return []; }
  210. }
  211. `;
  212. const { nodes, references } = nestjsResolver.extract!('users.controller.ts', src);
  213. expect(nodes).toHaveLength(1);
  214. expect(nodes[0].kind).toBe('route');
  215. expect(nodes[0].name).toBe('GET /users');
  216. expect(references[0].referenceName).toBe('findAll');
  217. expect(references[0].referenceKind).toBe('references');
  218. expect(references[0].fromNodeId).toBe(nodes[0].id);
  219. });
  220. it('joins controller prefix with a method-level path param', () => {
  221. const src = `
  222. @Controller('cats')
  223. export class CatsController {
  224. @Get(':id')
  225. findOne(@Param('id') id: string) { return id; }
  226. }
  227. `;
  228. const { nodes, references } = nestjsResolver.extract!('cats.controller.ts', src);
  229. expect(nodes[0].name).toBe('GET /cats/:id');
  230. expect(references[0].referenceName).toBe('findOne');
  231. });
  232. it('handles an empty @Controller() and empty @Post()', () => {
  233. const src = `
  234. @Controller()
  235. export class AppController {
  236. @Post()
  237. create() {}
  238. }
  239. `;
  240. const { nodes, references } = nestjsResolver.extract!('app.controller.ts', src);
  241. expect(nodes[0].name).toBe('POST /');
  242. expect(references[0].referenceName).toBe('create');
  243. });
  244. it('covers HTTP verbs and skips intervening method decorators', () => {
  245. const src = `
  246. @Controller('todos')
  247. export class TodosController {
  248. @Put(':id')
  249. @UseGuards(AuthGuard)
  250. update(@Param('id') id: string) {}
  251. @Delete(':id')
  252. async remove(@Param('id') id: string) {}
  253. }
  254. `;
  255. const { nodes, references } = nestjsResolver.extract!('todos.controller.ts', src);
  256. expect(nodes.map((n) => n.name)).toEqual(['PUT /todos/:id', 'DELETE /todos/:id']);
  257. expect(references.map((r) => r.referenceName)).toEqual(['update', 'remove']);
  258. });
  259. it('attributes methods to the right controller when a file has two', () => {
  260. const src = `
  261. @Controller('a')
  262. export class AController {
  263. @Get('x')
  264. ax() {}
  265. }
  266. @Controller('b')
  267. export class BController {
  268. @Get('y')
  269. by() {}
  270. }
  271. `;
  272. const { nodes } = nestjsResolver.extract!('multi.controller.ts', src);
  273. expect(nodes.map((n) => n.name)).toEqual(['GET /a/x', 'GET /b/y']);
  274. });
  275. });
  276. describe('nestjsResolver.extract — GraphQL', () => {
  277. it('emits QUERY/MUTATION nodes from a resolver, defaulting to the method name', () => {
  278. const src = `
  279. @Resolver(() => User)
  280. export class UsersResolver {
  281. @Query(() => [User])
  282. users() { return []; }
  283. @Mutation(() => User)
  284. createUser(@Args('input') input: CreateUserInput) {}
  285. }
  286. `;
  287. const { nodes, references } = nestjsResolver.extract!('users.resolver.ts', src);
  288. expect(nodes.map((n) => n.name)).toEqual(['QUERY users', 'MUTATION createUser']);
  289. expect(references.map((r) => r.referenceName)).toEqual(['users', 'createUser']);
  290. });
  291. it('uses an explicit operation name when given', () => {
  292. const src = `
  293. @Resolver()
  294. export class CatsResolver {
  295. @Query(() => Cat, { name: 'cat' })
  296. getCat() {}
  297. }
  298. `;
  299. const { nodes } = nestjsResolver.extract!('cats.resolver.ts', src);
  300. expect(nodes[0].name).toBe('QUERY cat');
  301. });
  302. it('does NOT treat the REST @Query() parameter decorator as a GraphQL op', () => {
  303. const src = `
  304. @Controller('search')
  305. export class SearchController {
  306. @Get()
  307. search(@Query() query: SearchDto) { return query; }
  308. }
  309. `;
  310. const { nodes } = nestjsResolver.extract!('search.controller.ts', src);
  311. // Only the HTTP route — the @Query() param decorator must be ignored.
  312. expect(nodes.map((n) => n.name)).toEqual(['GET /search']);
  313. });
  314. });
  315. describe('nestjsResolver.extract — microservices & websockets', () => {
  316. it('extracts @MessagePattern and @EventPattern handlers', () => {
  317. const src = `
  318. @Controller()
  319. export class MathController {
  320. @MessagePattern({ cmd: 'sum' })
  321. accumulate(data: number[]) {}
  322. @EventPattern('user.created')
  323. handleUserCreated(data: any) {}
  324. }
  325. `;
  326. const { nodes, references } = nestjsResolver.extract!('math.controller.ts', src);
  327. expect(nodes.map((n) => n.name)).toEqual(['MESSAGE sum', 'EVENT user.created']);
  328. expect(references.map((r) => r.referenceName)).toEqual(['accumulate', 'handleUserCreated']);
  329. });
  330. it('extracts @SubscribeMessage handlers with the gateway namespace', () => {
  331. const src = `
  332. @WebSocketGateway({ namespace: 'chat' })
  333. export class ChatGateway {
  334. @SubscribeMessage('message')
  335. handleMessage(@MessageBody() data: string) {}
  336. }
  337. `;
  338. const { nodes, references } = nestjsResolver.extract!('chat.gateway.ts', src);
  339. expect(nodes[0].name).toBe('WS chat:message');
  340. expect(references[0].referenceName).toBe('handleMessage');
  341. });
  342. it('extracts @SubscribeMessage without a namespace', () => {
  343. const src = `
  344. @WebSocketGateway()
  345. export class EventsGateway {
  346. @SubscribeMessage('events')
  347. onEvent() {}
  348. }
  349. `;
  350. const { nodes } = nestjsResolver.extract!('events.gateway.ts', src);
  351. expect(nodes[0].name).toBe('WS events');
  352. });
  353. it('returns empty for a non-JS/TS file', () => {
  354. const { nodes, references } = nestjsResolver.extract!('thing.py', '@Controller("x")');
  355. expect(nodes).toEqual([]);
  356. expect(references).toEqual([]);
  357. });
  358. });
  359. describe('nestjsResolver.detect', () => {
  360. const baseContext = {
  361. getNodesInFile: () => [],
  362. getNodesByName: () => [],
  363. getNodesByQualifiedName: () => [],
  364. getNodesByKind: () => [],
  365. fileExists: () => false,
  366. getProjectRoot: () => '/test',
  367. getAllFiles: () => [],
  368. getNodesByLowerName: () => [],
  369. getImportMappings: () => [],
  370. };
  371. it('detects @nestjs/* in package.json', () => {
  372. const context = {
  373. ...baseContext,
  374. readFile: (p: string) =>
  375. p === 'package.json'
  376. ? JSON.stringify({ dependencies: { '@nestjs/common': '^10.0.0' } })
  377. : null,
  378. };
  379. expect(nestjsResolver.detect(context as any)).toBe(true);
  380. });
  381. it('detects @Controller in a *.controller.ts file when package.json is absent', () => {
  382. const context = {
  383. ...baseContext,
  384. getAllFiles: () => ['src/users.controller.ts'],
  385. readFile: (p: string) =>
  386. p === 'src/users.controller.ts'
  387. ? `@Controller('users')\nexport class UsersController {}`
  388. : null,
  389. };
  390. expect(nestjsResolver.detect(context as any)).toBe(true);
  391. });
  392. it('returns false for a non-Nest project', () => {
  393. const context = {
  394. ...baseContext,
  395. readFile: (p: string) =>
  396. p === 'package.json' ? JSON.stringify({ dependencies: { express: '^4' } }) : null,
  397. };
  398. expect(nestjsResolver.detect(context as any)).toBe(false);
  399. });
  400. });
  401. describe('nestjsResolver.resolve', () => {
  402. const baseContext = {
  403. getNodesInFile: () => [],
  404. getNodesByName: () => [],
  405. getNodesByQualifiedName: () => [],
  406. getNodesByKind: () => [],
  407. fileExists: () => false,
  408. readFile: () => null,
  409. getProjectRoot: () => '/test',
  410. getAllFiles: () => [],
  411. getNodesByLowerName: () => [],
  412. getImportMappings: () => [],
  413. };
  414. it('resolves an injected *Service reference to the class in a *.service.ts file', () => {
  415. const svcNode: Node = {
  416. id: 'class:src/users/users.service.ts:UsersService:3',
  417. kind: 'class',
  418. name: 'UsersService',
  419. qualifiedName: 'src/users/users.service.ts::UsersService',
  420. filePath: 'src/users/users.service.ts',
  421. language: 'typescript',
  422. startLine: 3,
  423. endLine: 3,
  424. startColumn: 0,
  425. endColumn: 0,
  426. updatedAt: Date.now(),
  427. };
  428. const context = {
  429. ...baseContext,
  430. getNodesByName: (n: string) => (n === 'UsersService' ? [svcNode] : []),
  431. };
  432. const ref = {
  433. fromNodeId: 'class:src/users/users.controller.ts:UsersController:5',
  434. referenceName: 'UsersService',
  435. referenceKind: 'references' as const,
  436. line: 6,
  437. column: 4,
  438. filePath: 'src/users/users.controller.ts',
  439. language: 'typescript' as const,
  440. };
  441. const result = nestjsResolver.resolve(ref, context as any);
  442. expect(result?.targetNodeId).toBe(svcNode.id);
  443. expect(result?.resolvedBy).toBe('framework');
  444. expect(result?.confidence).toBeGreaterThanOrEqual(0.85);
  445. });
  446. it('returns null for a name without a provider suffix', () => {
  447. const ref = {
  448. fromNodeId: 'x',
  449. referenceName: 'doThing',
  450. referenceKind: 'references' as const,
  451. line: 1,
  452. column: 1,
  453. filePath: 'a.ts',
  454. language: 'typescript' as const,
  455. };
  456. expect(nestjsResolver.resolve(ref, baseContext as any)).toBeNull();
  457. });
  458. });
  459. import { laravelResolver } from '../src/resolution/frameworks/laravel';
  460. describe('laravelResolver.extract', () => {
  461. it('extracts route with controller tuple syntax', () => {
  462. const src = `Route::get('/users', [UserController::class, 'index']);\n`;
  463. const { nodes, references } = laravelResolver.extract!('routes/web.php', src);
  464. expect(nodes[0].name).toBe('GET /users');
  465. expect(references[0].referenceName).toBe('UserController@index');
  466. });
  467. it('extracts route with Controller@action syntax', () => {
  468. const src = `Route::post('/users', 'UserController@store');\n`;
  469. const { nodes, references } = laravelResolver.extract!('routes/web.php', src);
  470. expect(references[0].referenceName).toBe('UserController@store');
  471. });
  472. it('extracts resource route', () => {
  473. const src = `Route::resource('users', UserController::class);\n`;
  474. const { nodes, references } = laravelResolver.extract!('routes/web.php', src);
  475. expect(nodes[0].kind).toBe('route');
  476. expect(references[0].referenceName).toBe('UserController');
  477. });
  478. });
  479. import { railsResolver } from '../src/resolution/frameworks/ruby';
  480. describe('railsResolver.extract', () => {
  481. it('extracts route with controller#action syntax', () => {
  482. const src = `get '/users', to: 'users#index'\n`;
  483. const { nodes, references } = railsResolver.extract!('config/routes.rb', src);
  484. expect(nodes[0].name).toBe('GET /users');
  485. expect(references[0].referenceName).toBe('users#index');
  486. });
  487. it('extracts route without to: keyword', () => {
  488. const src = `post '/items' => 'items#create'\n`;
  489. const { nodes, references } = railsResolver.extract!('config/routes.rb', src);
  490. expect(references[0].referenceName).toBe('items#create');
  491. });
  492. });
  493. import { springResolver } from '../src/resolution/frameworks/java';
  494. describe('springResolver.extract', () => {
  495. it('extracts route with @GetMapping and next method', () => {
  496. const src = `
  497. @GetMapping("/users")
  498. public List<User> listUsers() {
  499. return users;
  500. }
  501. `;
  502. const { nodes, references } = springResolver.extract!('UserController.java', src);
  503. expect(nodes[0].name).toBe('GET /users');
  504. expect(references[0].referenceName).toBe('listUsers');
  505. });
  506. });
  507. import { goResolver } from '../src/resolution/frameworks/go';
  508. describe('goResolver.extract', () => {
  509. it('extracts route from r.GET', () => {
  510. const src = `r.GET("/users", listUsers)\n`;
  511. const { nodes, references } = goResolver.extract!('main.go', src);
  512. expect(nodes[0].name).toBe('GET /users');
  513. expect(references[0].referenceName).toBe('listUsers');
  514. });
  515. it('extracts route from router.HandleFunc', () => {
  516. const src = `router.HandleFunc("/items", createItem)\n`;
  517. const { nodes, references } = goResolver.extract!('main.go', src);
  518. expect(references[0].referenceName).toBe('createItem');
  519. });
  520. });
  521. import { rustResolver } from '../src/resolution/frameworks/rust';
  522. describe('rustResolver.extract', () => {
  523. it('extracts route from axum .route with get()', () => {
  524. const src = `let app = Router::new().route("/users", get(list_users));\n`;
  525. const { nodes, references } = rustResolver.extract!('main.rs', src);
  526. expect(nodes[0].name).toBe('GET /users');
  527. expect(references[0].referenceName).toBe('list_users');
  528. });
  529. it('extracts every method from a chained axum .route (get().put())', () => {
  530. const src = `let app = Router::new().route("/user", get(get_current_user).put(update_user));\n`;
  531. const { nodes, references } = rustResolver.extract!('main.rs', src);
  532. expect(nodes.map((n) => n.name)).toEqual(['GET /user', 'PUT /user']);
  533. expect(references.map((r) => r.referenceName)).toEqual([
  534. 'get_current_user',
  535. 'update_user',
  536. ]);
  537. });
  538. it('extracts a multi-line axum .route with a namespaced handler', () => {
  539. const src = `
  540. let app = Router::new()
  541. .route(
  542. "/articles/feed",
  543. get(listing::feed_articles),
  544. );
  545. `;
  546. const { nodes, references } = rustResolver.extract!('main.rs', src);
  547. expect(nodes[0].name).toBe('GET /articles/feed');
  548. expect(references[0].referenceName).toBe('feed_articles');
  549. });
  550. it('extracts actix web::resource().route(web::METHOD().to(handler))', () => {
  551. const src = `App::new().service(web::resource("/user/{id}").route(web::get().to(get_user)))\n`;
  552. const { nodes, references } = rustResolver.extract!('main.rs', src);
  553. expect(nodes[0].name).toBe('GET /user/{id}');
  554. expect(references[0].referenceName).toBe('get_user');
  555. });
  556. it('extracts actix web::resource("/").to(handler) (all methods)', () => {
  557. const src = `App::new().service(web::resource("/").to(index))\n`;
  558. const { nodes, references } = rustResolver.extract!('main.rs', src);
  559. expect(nodes[0].name).toBe('ANY /');
  560. expect(references[0].referenceName).toBe('index');
  561. });
  562. it('extracts actix App-level .route("/path", web::METHOD().to(handler))', () => {
  563. const src = `App::new().route("/health", web::get().to(health_check))\n`;
  564. const { nodes, references } = rustResolver.extract!('main.rs', src);
  565. expect(nodes[0].name).toBe('GET /health');
  566. expect(references[0].referenceName).toBe('health_check');
  567. });
  568. });
  569. describe('rustResolver.resolve cargo workspace crates', () => {
  570. it('resolves crate name from workspace member lib.rs', () => {
  571. const workspaceCargo = `
  572. [workspace]
  573. members = ["crates/mytool-core", "crates/mytool-fetcher"]
  574. `;
  575. const coreCargo = `
  576. [package]
  577. name = "mytool-core"
  578. version = "0.1.0"
  579. `;
  580. const libNode: Node = {
  581. id: 'module:crates/mytool-core/src/lib.rs:mytool_core:1',
  582. kind: 'module',
  583. name: 'mytool_core',
  584. qualifiedName: 'crates/mytool-core/src/lib.rs::mytool_core',
  585. filePath: 'crates/mytool-core/src/lib.rs',
  586. language: 'rust',
  587. startLine: 1,
  588. endLine: 1,
  589. startColumn: 0,
  590. endColumn: 0,
  591. updatedAt: Date.now(),
  592. };
  593. const context = {
  594. getNodesInFile: (fp: string) => (fp === 'crates/mytool-core/src/lib.rs' ? [libNode] : []),
  595. getNodesByName: () => [],
  596. getNodesByQualifiedName: () => [],
  597. getNodesByKind: () => [],
  598. fileExists: (p: string) => (
  599. p === 'Cargo.toml' ||
  600. p === 'crates/mytool-core/Cargo.toml' ||
  601. p === 'crates/mytool-core/src/lib.rs'
  602. ),
  603. readFile: (p: string) => {
  604. if (p === 'Cargo.toml') return workspaceCargo;
  605. if (p === 'crates/mytool-core/Cargo.toml') return coreCargo;
  606. return null;
  607. },
  608. getProjectRoot: () => '/test',
  609. getAllFiles: () => [
  610. 'Cargo.toml',
  611. 'crates/mytool-core/Cargo.toml',
  612. 'crates/mytool-core/src/lib.rs',
  613. ],
  614. getNodesByLowerName: () => [],
  615. getImportMappings: () => [],
  616. };
  617. const ref = {
  618. fromNodeId: 'fn:crates/mytool-fetcher/src/main.rs:main:1',
  619. referenceName: 'mytool_core',
  620. referenceKind: 'references' as const,
  621. line: 1,
  622. column: 1,
  623. filePath: 'crates/mytool-fetcher/src/main.rs',
  624. language: 'rust' as const,
  625. };
  626. const result = rustResolver.resolve(ref, context);
  627. expect(result?.targetNodeId).toBe(libNode.id);
  628. expect(result?.resolvedBy).toBe('framework');
  629. // Workspace-manifest hits are unambiguous and must beat name-matcher's
  630. // self-file matches (0.7) so cross-crate `imports` edges materialize.
  631. expect(result?.confidence).toBeGreaterThanOrEqual(0.9);
  632. });
  633. it('resolves crate name from workspace member main.rs when lib.rs is absent', () => {
  634. const workspaceCargo = `
  635. [workspace]
  636. members = [
  637. "crates/mytool-runner",
  638. ]
  639. `;
  640. const runnerCargo = `
  641. [package]
  642. name = "mytool-runner"
  643. version = "0.1.0"
  644. `;
  645. const mainNode: Node = {
  646. id: 'module:crates/mytool-runner/src/main.rs:mytool_runner:1',
  647. kind: 'module',
  648. name: 'mytool_runner',
  649. qualifiedName: 'crates/mytool-runner/src/main.rs::mytool_runner',
  650. filePath: 'crates/mytool-runner/src/main.rs',
  651. language: 'rust',
  652. startLine: 1,
  653. endLine: 1,
  654. startColumn: 0,
  655. endColumn: 0,
  656. updatedAt: Date.now(),
  657. };
  658. const context = {
  659. getNodesInFile: (fp: string) => (fp === 'crates/mytool-runner/src/main.rs' ? [mainNode] : []),
  660. getNodesByName: () => [],
  661. getNodesByQualifiedName: () => [],
  662. getNodesByKind: () => [],
  663. fileExists: (p: string) => (
  664. p === 'Cargo.toml' ||
  665. p === 'crates/mytool-runner/Cargo.toml' ||
  666. p === 'crates/mytool-runner/src/main.rs'
  667. ),
  668. readFile: (p: string) => {
  669. if (p === 'Cargo.toml') return workspaceCargo;
  670. if (p === 'crates/mytool-runner/Cargo.toml') return runnerCargo;
  671. return null;
  672. },
  673. getProjectRoot: () => '/test',
  674. getAllFiles: () => [
  675. 'Cargo.toml',
  676. 'crates/mytool-runner/Cargo.toml',
  677. 'crates/mytool-runner/src/main.rs',
  678. ],
  679. getNodesByLowerName: () => [],
  680. getImportMappings: () => [],
  681. };
  682. const ref = {
  683. fromNodeId: 'fn:crates/mytool-runner/src/main.rs:main:1',
  684. referenceName: 'mytool_runner',
  685. referenceKind: 'references' as const,
  686. line: 1,
  687. column: 1,
  688. filePath: 'crates/mytool-runner/src/main.rs',
  689. language: 'rust' as const,
  690. };
  691. const result = rustResolver.resolve(ref, context);
  692. expect(result?.targetNodeId).toBe(mainNode.id);
  693. expect(result?.resolvedBy).toBe('framework');
  694. });
  695. it('resolves crate name when members uses a glob (crates/*)', () => {
  696. const workspaceCargo = `
  697. [workspace]
  698. members = ["crates/*"]
  699. `;
  700. const fooCargo = `
  701. [package]
  702. name = "mytool-foo"
  703. version = "0.1.0"
  704. `;
  705. const barCargo = `
  706. [package]
  707. name = "mytool-bar"
  708. version = "0.1.0"
  709. `;
  710. const fooLib: Node = {
  711. id: 'module:crates/mytool-foo/src/lib.rs:mytool_foo:1',
  712. kind: 'module',
  713. name: 'mytool_foo',
  714. qualifiedName: 'crates/mytool-foo/src/lib.rs::mytool_foo',
  715. filePath: 'crates/mytool-foo/src/lib.rs',
  716. language: 'rust',
  717. startLine: 1,
  718. endLine: 1,
  719. startColumn: 0,
  720. endColumn: 0,
  721. updatedAt: Date.now(),
  722. };
  723. const barLib: Node = {
  724. id: 'module:crates/mytool-bar/src/lib.rs:mytool_bar:1',
  725. kind: 'module',
  726. name: 'mytool_bar',
  727. qualifiedName: 'crates/mytool-bar/src/lib.rs::mytool_bar',
  728. filePath: 'crates/mytool-bar/src/lib.rs',
  729. language: 'rust',
  730. startLine: 1,
  731. endLine: 1,
  732. startColumn: 0,
  733. endColumn: 0,
  734. updatedAt: Date.now(),
  735. };
  736. const filesByPath: Record<string, string> = {
  737. 'Cargo.toml': workspaceCargo,
  738. 'crates/mytool-foo/Cargo.toml': fooCargo,
  739. 'crates/mytool-bar/Cargo.toml': barCargo,
  740. };
  741. const nodesByFile: Record<string, Node[]> = {
  742. 'crates/mytool-foo/src/lib.rs': [fooLib],
  743. 'crates/mytool-bar/src/lib.rs': [barLib],
  744. };
  745. const dirsByPath: Record<string, string[]> = {
  746. '.': ['crates'],
  747. crates: ['mytool-foo', 'mytool-bar'],
  748. 'crates/mytool-foo': ['src'],
  749. 'crates/mytool-bar': ['src'],
  750. };
  751. const context = {
  752. getNodesInFile: (fp: string) => nodesByFile[fp] ?? [],
  753. getNodesByName: () => [],
  754. getNodesByQualifiedName: () => [],
  755. getNodesByKind: () => [],
  756. fileExists: (p: string) => (
  757. Object.prototype.hasOwnProperty.call(filesByPath, p) ||
  758. Object.prototype.hasOwnProperty.call(nodesByFile, p)
  759. ),
  760. readFile: (p: string) => filesByPath[p] ?? null,
  761. getProjectRoot: () => '/test',
  762. getAllFiles: () => [
  763. 'Cargo.toml',
  764. ...Object.keys(filesByPath).filter((p) => p !== 'Cargo.toml'),
  765. ...Object.keys(nodesByFile),
  766. ],
  767. getNodesByLowerName: () => [],
  768. getImportMappings: () => [],
  769. listDirectories: (rel: string) => dirsByPath[rel] ?? [],
  770. };
  771. const fooRef = {
  772. fromNodeId: 'fn:crates/mytool-bar/src/lib.rs:other:1',
  773. referenceName: 'mytool_foo',
  774. referenceKind: 'references' as const,
  775. line: 1,
  776. column: 1,
  777. filePath: 'crates/mytool-bar/src/lib.rs',
  778. language: 'rust' as const,
  779. };
  780. const barRef = {
  781. fromNodeId: 'fn:crates/mytool-foo/src/lib.rs:other:1',
  782. referenceName: 'mytool_bar',
  783. referenceKind: 'references' as const,
  784. line: 1,
  785. column: 1,
  786. filePath: 'crates/mytool-foo/src/lib.rs',
  787. language: 'rust' as const,
  788. };
  789. expect(rustResolver.resolve(fooRef, context)?.targetNodeId).toBe(fooLib.id);
  790. expect(rustResolver.resolve(barRef, context)?.targetNodeId).toBe(barLib.id);
  791. });
  792. it('resolves crate name when members uses a name glob at root (helix-*)', () => {
  793. const workspaceCargo = `
  794. [workspace]
  795. members = ["helix-*"]
  796. `;
  797. const coreCargo = `
  798. [package]
  799. name = "helix-core"
  800. version = "0.1.0"
  801. `;
  802. const coreLib: Node = {
  803. id: 'module:helix-core/src/lib.rs:helix_core:1',
  804. kind: 'module',
  805. name: 'helix_core',
  806. qualifiedName: 'helix-core/src/lib.rs::helix_core',
  807. filePath: 'helix-core/src/lib.rs',
  808. language: 'rust',
  809. startLine: 1,
  810. endLine: 1,
  811. startColumn: 0,
  812. endColumn: 0,
  813. updatedAt: Date.now(),
  814. };
  815. const filesByPath: Record<string, string> = {
  816. 'Cargo.toml': workspaceCargo,
  817. 'helix-core/Cargo.toml': coreCargo,
  818. };
  819. const nodesByFile: Record<string, Node[]> = {
  820. 'helix-core/src/lib.rs': [coreLib],
  821. };
  822. const dirsByPath: Record<string, string[]> = {
  823. '.': ['helix-core', 'docs', 'target'],
  824. 'helix-core': ['src'],
  825. };
  826. const context = {
  827. getNodesInFile: (fp: string) => nodesByFile[fp] ?? [],
  828. getNodesByName: () => [],
  829. getNodesByQualifiedName: () => [],
  830. getNodesByKind: () => [],
  831. fileExists: (p: string) => (
  832. Object.prototype.hasOwnProperty.call(filesByPath, p) ||
  833. Object.prototype.hasOwnProperty.call(nodesByFile, p)
  834. ),
  835. readFile: (p: string) => filesByPath[p] ?? null,
  836. getProjectRoot: () => '/test',
  837. getAllFiles: () => [
  838. 'Cargo.toml',
  839. ...Object.keys(filesByPath).filter((p) => p !== 'Cargo.toml'),
  840. ...Object.keys(nodesByFile),
  841. ],
  842. getNodesByLowerName: () => [],
  843. getImportMappings: () => [],
  844. listDirectories: (rel: string) => dirsByPath[rel] ?? [],
  845. };
  846. const ref = {
  847. fromNodeId: 'fn:helix-core/src/lib.rs:other:1',
  848. referenceName: 'helix_core',
  849. referenceKind: 'references' as const,
  850. line: 1,
  851. column: 1,
  852. filePath: 'helix-core/src/lib.rs',
  853. language: 'rust' as const,
  854. };
  855. expect(rustResolver.resolve(ref, context)?.targetNodeId).toBe(coreLib.id);
  856. });
  857. });
  858. import { aspnetResolver } from '../src/resolution/frameworks/csharp';
  859. describe('aspnetResolver.extract', () => {
  860. it('extracts route from [HttpGet] attribute', () => {
  861. const src = `
  862. [HttpGet("/users")]
  863. public IActionResult ListUsers()
  864. {
  865. return Ok();
  866. }
  867. `;
  868. const { nodes, references } = aspnetResolver.extract!('UserController.cs', src);
  869. expect(nodes[0].name).toBe('GET /users');
  870. expect(references[0].referenceName).toBe('ListUsers');
  871. });
  872. });
  873. import { vaporResolver } from '../src/resolution/frameworks/swift';
  874. describe('vaporResolver.extract', () => {
  875. it('extracts route from app.get with use:', () => {
  876. const src = `app.get("users", use: listUsers)\n`;
  877. const { nodes, references } = vaporResolver.extract!('routes.swift', src);
  878. expect(nodes[0].name).toBe('GET /users');
  879. expect(references[0].referenceName).toBe('listUsers');
  880. });
  881. it('extracts grouped RouteCollection routes with the group prefix and no path arg', () => {
  882. const src = `
  883. func boot(routes: RoutesBuilder) throws {
  884. let todos = routes.grouped("todos")
  885. todos.get(use: index)
  886. todos.post(use: create)
  887. todos.group(":todoID") { todo in
  888. todo.delete(use: delete)
  889. }
  890. }
  891. `;
  892. const { nodes, references } = vaporResolver.extract!('TodoController.swift', src);
  893. expect(nodes.map((n) => n.name).sort()).toEqual([
  894. 'DELETE /todos/:todoID',
  895. 'GET /todos',
  896. 'POST /todos',
  897. ]);
  898. expect(references.map((r) => r.referenceName).sort()).toEqual([
  899. 'create',
  900. 'delete',
  901. 'index',
  902. ]);
  903. });
  904. it('handles use: self.handler and non-string path segments', () => {
  905. const src = `router.get("users", User.parameter, "edit", use: self.editUserHandler)\n`;
  906. const { nodes, references } = vaporResolver.extract!('UserController.swift', src);
  907. expect(nodes[0].name).toBe('GET /users/edit');
  908. expect(references[0].referenceName).toBe('editUserHandler');
  909. });
  910. it('ignores non-route .get calls that lack use: (e.g. Environment.get)', () => {
  911. const src = `let host = Environment.get("DATABASE_HOST") ?? "localhost"\n`;
  912. const { nodes } = vaporResolver.extract!('configure.swift', src);
  913. expect(nodes).toHaveLength(0);
  914. });
  915. });
  916. import { reactResolver } from '../src/resolution/frameworks/react';
  917. import { svelteResolver } from '../src/resolution/frameworks/svelte';
  918. describe('reactResolver.extract — React Router', () => {
  919. it('extracts a v6 <Route path element={<Comp/>}>', () => {
  920. const src = `<Route path="/users" element={<UsersPage/>}/>`;
  921. const { nodes, references } = reactResolver.extract!('App.tsx', src);
  922. const route = nodes.find((n) => n.kind === 'route');
  923. expect(route?.name).toBe('/users');
  924. expect(references[0]?.referenceName).toBe('UsersPage');
  925. });
  926. it('extracts a v5 <Route path component={Comp}> with attributes in any order', () => {
  927. const src = `<Route exact path="/login" component={Login} />`;
  928. const { nodes, references } = reactResolver.extract!('App.jsx', src);
  929. const route = nodes.find((n) => n.kind === 'route');
  930. expect(route?.name).toBe('/login');
  931. expect(references[0]?.referenceName).toBe('Login');
  932. });
  933. it('does not treat the <Routes> container as a route', () => {
  934. const src = `<Routes><Route path="/x" element={<X/>}/></Routes>`;
  935. const routes = reactResolver.extract!('App.tsx', src).nodes.filter((n) => n.kind === 'route');
  936. expect(routes).toHaveLength(1);
  937. expect(routes[0]?.name).toBe('/x');
  938. });
  939. });
  940. describe('svelteResolver.extract (smoke)', () => {
  941. it('returns { nodes, references } shape', () => {
  942. const result = svelteResolver.extract!('+page.svelte', '');
  943. expect(result).toHaveProperty('nodes');
  944. expect(result).toHaveProperty('references');
  945. });
  946. });
  947. // Regression tests: commented-out and docstring route examples must NOT
  948. // surface as phantom route nodes. These would have failed before the
  949. // strip-comments wiring (the regex would happily scan comments/docstrings).
  950. describe('framework extractors ignore commented-out routes', () => {
  951. it('django: skips line-comment and docstring routes', () => {
  952. const src = `
  953. # urls.py example:
  954. # path('/admin/', AdminPanel.as_view())
  955. """
  956. Other routing example:
  957. path('/users/', UserListView.as_view())
  958. """
  959. urlpatterns = [path('/real/', RealView.as_view())]
  960. `;
  961. const result = djangoResolver.extract!('app/urls.py', src);
  962. const urls = result.nodes.map((n) => n.name);
  963. expect(urls).toEqual(['/real/']);
  964. });
  965. it('flask: skips commented-out @app.route', () => {
  966. const src = `
  967. # @app.route('/fake')
  968. # def fake_view():
  969. # return ''
  970. @app.route('/real')
  971. def real_view():
  972. return ''
  973. `;
  974. const { nodes, references } = flaskResolver.extract!('app.py', src);
  975. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  976. expect(references.map((r) => r.referenceName)).toEqual(['real_view']);
  977. });
  978. it('fastapi: skips docstring example routes', () => {
  979. const src = `
  980. """
  981. Example:
  982. @app.get('/in-docstring')
  983. async def doc():
  984. pass
  985. """
  986. @app.get('/real')
  987. async def real_handler():
  988. return {}
  989. `;
  990. const { nodes, references } = fastapiResolver.extract!('main.py', src);
  991. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  992. expect(references.map((r) => r.referenceName)).toEqual(['real_handler']);
  993. });
  994. it('express: skips // and /* */ commented routes', () => {
  995. const src = `
  996. // app.get('/fake', fakeHandler);
  997. /* router.post('/also-fake', otherHandler); */
  998. app.get('/real', realHandler);
  999. `;
  1000. const { nodes, references } = expressResolver.extract!('routes.ts', src);
  1001. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1002. expect(references.map((r) => r.referenceName)).toEqual(['realHandler']);
  1003. });
  1004. it('laravel: skips // # and /* */ commented Route::* calls', () => {
  1005. const src = `<?php
  1006. // Route::get('/fake', [FakeController::class, 'index']);
  1007. # Route::get('/also-fake', 'FakeController@show');
  1008. /* Route::post('/another-fake', [X::class, 'y']); */
  1009. Route::get('/real', [RealController::class, 'index']);
  1010. `;
  1011. const { nodes, references } = laravelResolver.extract!('routes/web.php', src);
  1012. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1013. expect(references.map((r) => r.referenceName)).toEqual(['RealController@index']);
  1014. });
  1015. it('rails: skips =begin/=end and # commented routes', () => {
  1016. const src = `
  1017. # get '/fake', to: 'fake#index'
  1018. =begin
  1019. get '/also-fake', to: 'fake#show'
  1020. =end
  1021. get '/real', to: 'real#index'
  1022. `;
  1023. const { nodes, references } = railsResolver.extract!('config/routes.rb', src);
  1024. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1025. expect(references.map((r) => r.referenceName)).toEqual(['real#index']);
  1026. });
  1027. it('spring: skips // and /* */ commented @GetMapping', () => {
  1028. const src = `
  1029. // @GetMapping("/fake")
  1030. // public List<X> fake() { return null; }
  1031. /* @PostMapping("/also-fake")
  1032. public void alsoFake() {} */
  1033. @GetMapping("/real")
  1034. public List<User> listUsers() { return users; }
  1035. `;
  1036. const { nodes, references } = springResolver.extract!('UserController.java', src);
  1037. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1038. expect(references.map((r) => r.referenceName)).toEqual(['listUsers']);
  1039. });
  1040. it('go: skips // and /* */ commented router.METHOD calls', () => {
  1041. const src = `
  1042. // r.GET("/fake", fakeHandler)
  1043. /* r.POST("/also-fake", anotherHandler) */
  1044. r.GET("/real", listUsers)
  1045. `;
  1046. const { nodes, references } = goResolver.extract!('main.go', src);
  1047. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1048. expect(references.map((r) => r.referenceName)).toEqual(['listUsers']);
  1049. });
  1050. it('rust: skips // and nested /* */ commented .route() calls', () => {
  1051. const src = `
  1052. // .route("/fake", get(fake_handler))
  1053. /* outer /* inner .route("/inner-fake", get(x)) */ still .route("/outer-fake", get(y)) */
  1054. let app = Router::new().route("/real", get(list_users));
  1055. `;
  1056. const { nodes, references } = rustResolver.extract!('main.rs', src);
  1057. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1058. expect(references.map((r) => r.referenceName)).toEqual(['list_users']);
  1059. });
  1060. it('aspnet: skips // and /* */ commented [HttpGet] attributes', () => {
  1061. const src = `
  1062. // [HttpGet("/fake")]
  1063. // public IActionResult Fake() { return Ok(); }
  1064. /* [HttpPost("/also-fake")]
  1065. public IActionResult AlsoFake() { return Ok(); } */
  1066. [HttpGet("/real")]
  1067. public IActionResult ListUsers() { return Ok(); }
  1068. `;
  1069. const { nodes, references } = aspnetResolver.extract!('UserController.cs', src);
  1070. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1071. expect(references.map((r) => r.referenceName)).toEqual(['ListUsers']);
  1072. });
  1073. it('vapor: skips // and /* */ commented app.METHOD calls', () => {
  1074. const src = `
  1075. // app.get("fake", use: fakeHandler)
  1076. /* app.post("also-fake", use: anotherHandler) */
  1077. app.get("real", use: listUsers)
  1078. `;
  1079. const { nodes, references } = vaporResolver.extract!('routes.swift', src);
  1080. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1081. expect(references.map((r) => r.referenceName)).toEqual(['listUsers']);
  1082. });
  1083. it('nestjs: skips // and /* */ commented decorators', () => {
  1084. const src = `
  1085. @Controller('users')
  1086. export class UsersController {
  1087. // @Get('fake')
  1088. // fake() {}
  1089. /* @Post('also-fake')
  1090. alsoFake() {} */
  1091. @Get('real')
  1092. real() {}
  1093. }
  1094. `;
  1095. const { nodes, references } = nestjsResolver.extract!('users.controller.ts', src);
  1096. expect(nodes.map((n) => n.name)).toEqual(['GET /users/real']);
  1097. expect(references.map((r) => r.referenceName)).toEqual(['real']);
  1098. });
  1099. });