frameworks.test.ts 61 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774
  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. it('extracts the method from a tuple methods=(...) (not just a list)', () => {
  134. const src = `
  135. @blueprint.route('/api/articles', methods=('POST',))
  136. def make_article():
  137. pass
  138. `;
  139. const { nodes, references } = flaskResolver.extract!('views.py', src);
  140. expect(nodes[0].name).toBe('POST /api/articles');
  141. expect(references[0].referenceName).toBe('make_article');
  142. });
  143. it('extracts Flask-RESTful api.add_resource(Resource, paths) → the Resource class', () => {
  144. const src = `
  145. api.add_resource(TodoResource, '/todos/<id>')
  146. api.add_org_resource(AlertResource, '/api/alerts/<id>', endpoint='alert')
  147. `;
  148. const { nodes, references } = flaskResolver.extract!('api.py', src);
  149. expect(nodes.map((n) => n.name)).toEqual(['ANY /todos/<id>', 'ANY /api/alerts/<id>']);
  150. expect(references.map((r) => r.referenceName)).toEqual(['TodoResource', 'AlertResource']);
  151. });
  152. });
  153. describe('fastapiResolver.extract', () => {
  154. it('extracts route and reference from @app.get', () => {
  155. const src = `
  156. @app.get('/users')
  157. async def list_users():
  158. return []
  159. `;
  160. const { nodes, references } = fastapiResolver.extract!('main.py', src);
  161. expect(nodes[0].name).toBe('GET /users');
  162. expect(references[0].referenceName).toBe('list_users');
  163. });
  164. it('extracts route from router.post', () => {
  165. const src = `
  166. @router.post('/items')
  167. def create_item(item: Item):
  168. pass
  169. `;
  170. const { nodes, references } = fastapiResolver.extract!('items.py', src);
  171. expect(nodes[0].name).toBe('POST /items');
  172. expect(references[0].referenceName).toBe('create_item');
  173. });
  174. it('extracts a route mounted at the router/prefix root (empty path)', () => {
  175. const src = `
  176. @router.get("", response_model=ListOfArticles, name="articles:list")
  177. async def list_articles():
  178. return []
  179. `;
  180. const { nodes, references } = fastapiResolver.extract!('articles.py', src);
  181. expect(nodes[0].name).toBe('GET /');
  182. expect(references[0].referenceName).toBe('list_articles');
  183. });
  184. it('extracts a multi-line decorator with an empty path', () => {
  185. const src = `
  186. @router.post(
  187. "",
  188. status_code=201,
  189. response_model=ArticleInResponse,
  190. )
  191. async def create_article():
  192. pass
  193. `;
  194. const { nodes, references } = fastapiResolver.extract!('articles.py', src);
  195. expect(nodes[0].name).toBe('POST /');
  196. expect(references[0].referenceName).toBe('create_article');
  197. });
  198. });
  199. import { expressResolver } from '../src/resolution/frameworks/express';
  200. describe('expressResolver.extract', () => {
  201. it('extracts route with inline handler reference', () => {
  202. const src = `app.get('/users', listUsers);\n`;
  203. const { nodes, references } = expressResolver.extract!('routes.ts', src);
  204. expect(nodes).toHaveLength(1);
  205. expect(nodes[0].name).toBe('GET /users');
  206. expect(references[0].referenceName).toBe('listUsers');
  207. });
  208. it('extracts route with router.post and middleware chain', () => {
  209. const src = `router.post('/items', auth, createItem);\n`;
  210. const { nodes, references } = expressResolver.extract!('items.ts', src);
  211. expect(nodes[0].name).toBe('POST /items');
  212. // Multiple handlers: prefer the LAST one (convention: middleware first, handler last)
  213. expect(references[0].referenceName).toBe('createItem');
  214. });
  215. it('extracts route with controller method reference', () => {
  216. const src = `app.get('/x', userController.list);\n`;
  217. const { nodes, references } = expressResolver.extract!('routes.ts', src);
  218. expect(references[0].referenceName).toBe('list');
  219. });
  220. });
  221. import { nestjsResolver } from '../src/resolution/frameworks/nestjs';
  222. describe('nestjsResolver.extract — HTTP', () => {
  223. it('joins @Controller prefix with @Get and links the handler', () => {
  224. const src = `
  225. @Controller('users')
  226. export class UsersController {
  227. @Get()
  228. findAll() { return []; }
  229. }
  230. `;
  231. const { nodes, references } = nestjsResolver.extract!('users.controller.ts', src);
  232. expect(nodes).toHaveLength(1);
  233. expect(nodes[0].kind).toBe('route');
  234. expect(nodes[0].name).toBe('GET /users');
  235. expect(references[0].referenceName).toBe('findAll');
  236. expect(references[0].referenceKind).toBe('references');
  237. expect(references[0].fromNodeId).toBe(nodes[0].id);
  238. });
  239. it('joins controller prefix with a method-level path param', () => {
  240. const src = `
  241. @Controller('cats')
  242. export class CatsController {
  243. @Get(':id')
  244. findOne(@Param('id') id: string) { return id; }
  245. }
  246. `;
  247. const { nodes, references } = nestjsResolver.extract!('cats.controller.ts', src);
  248. expect(nodes[0].name).toBe('GET /cats/:id');
  249. expect(references[0].referenceName).toBe('findOne');
  250. });
  251. it('handles an empty @Controller() and empty @Post()', () => {
  252. const src = `
  253. @Controller()
  254. export class AppController {
  255. @Post()
  256. create() {}
  257. }
  258. `;
  259. const { nodes, references } = nestjsResolver.extract!('app.controller.ts', src);
  260. expect(nodes[0].name).toBe('POST /');
  261. expect(references[0].referenceName).toBe('create');
  262. });
  263. it('covers HTTP verbs and skips intervening method decorators', () => {
  264. const src = `
  265. @Controller('todos')
  266. export class TodosController {
  267. @Put(':id')
  268. @UseGuards(AuthGuard)
  269. update(@Param('id') id: string) {}
  270. @Delete(':id')
  271. async remove(@Param('id') id: string) {}
  272. }
  273. `;
  274. const { nodes, references } = nestjsResolver.extract!('todos.controller.ts', src);
  275. expect(nodes.map((n) => n.name)).toEqual(['PUT /todos/:id', 'DELETE /todos/:id']);
  276. expect(references.map((r) => r.referenceName)).toEqual(['update', 'remove']);
  277. });
  278. it('attributes methods to the right controller when a file has two', () => {
  279. const src = `
  280. @Controller('a')
  281. export class AController {
  282. @Get('x')
  283. ax() {}
  284. }
  285. @Controller('b')
  286. export class BController {
  287. @Get('y')
  288. by() {}
  289. }
  290. `;
  291. const { nodes } = nestjsResolver.extract!('multi.controller.ts', src);
  292. expect(nodes.map((n) => n.name)).toEqual(['GET /a/x', 'GET /b/y']);
  293. });
  294. });
  295. describe('nestjsResolver.extract — GraphQL', () => {
  296. it('emits QUERY/MUTATION nodes from a resolver, defaulting to the method name', () => {
  297. const src = `
  298. @Resolver(() => User)
  299. export class UsersResolver {
  300. @Query(() => [User])
  301. users() { return []; }
  302. @Mutation(() => User)
  303. createUser(@Args('input') input: CreateUserInput) {}
  304. }
  305. `;
  306. const { nodes, references } = nestjsResolver.extract!('users.resolver.ts', src);
  307. expect(nodes.map((n) => n.name)).toEqual(['QUERY users', 'MUTATION createUser']);
  308. expect(references.map((r) => r.referenceName)).toEqual(['users', 'createUser']);
  309. });
  310. it('uses an explicit operation name when given', () => {
  311. const src = `
  312. @Resolver()
  313. export class CatsResolver {
  314. @Query(() => Cat, { name: 'cat' })
  315. getCat() {}
  316. }
  317. `;
  318. const { nodes } = nestjsResolver.extract!('cats.resolver.ts', src);
  319. expect(nodes[0].name).toBe('QUERY cat');
  320. });
  321. it('does NOT treat the REST @Query() parameter decorator as a GraphQL op', () => {
  322. const src = `
  323. @Controller('search')
  324. export class SearchController {
  325. @Get()
  326. search(@Query() query: SearchDto) { return query; }
  327. }
  328. `;
  329. const { nodes } = nestjsResolver.extract!('search.controller.ts', src);
  330. // Only the HTTP route — the @Query() param decorator must be ignored.
  331. expect(nodes.map((n) => n.name)).toEqual(['GET /search']);
  332. });
  333. });
  334. describe('nestjsResolver.extract — microservices & websockets', () => {
  335. it('extracts @MessagePattern and @EventPattern handlers', () => {
  336. const src = `
  337. @Controller()
  338. export class MathController {
  339. @MessagePattern({ cmd: 'sum' })
  340. accumulate(data: number[]) {}
  341. @EventPattern('user.created')
  342. handleUserCreated(data: any) {}
  343. }
  344. `;
  345. const { nodes, references } = nestjsResolver.extract!('math.controller.ts', src);
  346. expect(nodes.map((n) => n.name)).toEqual(['MESSAGE sum', 'EVENT user.created']);
  347. expect(references.map((r) => r.referenceName)).toEqual(['accumulate', 'handleUserCreated']);
  348. });
  349. it('extracts @SubscribeMessage handlers with the gateway namespace', () => {
  350. const src = `
  351. @WebSocketGateway({ namespace: 'chat' })
  352. export class ChatGateway {
  353. @SubscribeMessage('message')
  354. handleMessage(@MessageBody() data: string) {}
  355. }
  356. `;
  357. const { nodes, references } = nestjsResolver.extract!('chat.gateway.ts', src);
  358. expect(nodes[0].name).toBe('WS chat:message');
  359. expect(references[0].referenceName).toBe('handleMessage');
  360. });
  361. it('extracts @SubscribeMessage without a namespace', () => {
  362. const src = `
  363. @WebSocketGateway()
  364. export class EventsGateway {
  365. @SubscribeMessage('events')
  366. onEvent() {}
  367. }
  368. `;
  369. const { nodes } = nestjsResolver.extract!('events.gateway.ts', src);
  370. expect(nodes[0].name).toBe('WS events');
  371. });
  372. it('returns empty for a non-JS/TS file', () => {
  373. const { nodes, references } = nestjsResolver.extract!('thing.py', '@Controller("x")');
  374. expect(nodes).toEqual([]);
  375. expect(references).toEqual([]);
  376. });
  377. });
  378. describe('nestjsResolver.detect', () => {
  379. const baseContext = {
  380. getNodesInFile: () => [],
  381. getNodesByName: () => [],
  382. getNodesByQualifiedName: () => [],
  383. getNodesByKind: () => [],
  384. fileExists: () => false,
  385. getProjectRoot: () => '/test',
  386. getAllFiles: () => [],
  387. getNodesByLowerName: () => [],
  388. getImportMappings: () => [],
  389. };
  390. it('detects @nestjs/* in package.json', () => {
  391. const context = {
  392. ...baseContext,
  393. readFile: (p: string) =>
  394. p === 'package.json'
  395. ? JSON.stringify({ dependencies: { '@nestjs/common': '^10.0.0' } })
  396. : null,
  397. };
  398. expect(nestjsResolver.detect(context as any)).toBe(true);
  399. });
  400. it('detects @Controller in a *.controller.ts file when package.json is absent', () => {
  401. const context = {
  402. ...baseContext,
  403. getAllFiles: () => ['src/users.controller.ts'],
  404. readFile: (p: string) =>
  405. p === 'src/users.controller.ts'
  406. ? `@Controller('users')\nexport class UsersController {}`
  407. : null,
  408. };
  409. expect(nestjsResolver.detect(context as any)).toBe(true);
  410. });
  411. it('returns false for a non-Nest project', () => {
  412. const context = {
  413. ...baseContext,
  414. readFile: (p: string) =>
  415. p === 'package.json' ? JSON.stringify({ dependencies: { express: '^4' } }) : null,
  416. };
  417. expect(nestjsResolver.detect(context as any)).toBe(false);
  418. });
  419. });
  420. describe('nestjsResolver.resolve', () => {
  421. const baseContext = {
  422. getNodesInFile: () => [],
  423. getNodesByName: () => [],
  424. getNodesByQualifiedName: () => [],
  425. getNodesByKind: () => [],
  426. fileExists: () => false,
  427. readFile: () => null,
  428. getProjectRoot: () => '/test',
  429. getAllFiles: () => [],
  430. getNodesByLowerName: () => [],
  431. getImportMappings: () => [],
  432. };
  433. it('resolves an injected *Service reference to the class in a *.service.ts file', () => {
  434. const svcNode: Node = {
  435. id: 'class:src/users/users.service.ts:UsersService:3',
  436. kind: 'class',
  437. name: 'UsersService',
  438. qualifiedName: 'src/users/users.service.ts::UsersService',
  439. filePath: 'src/users/users.service.ts',
  440. language: 'typescript',
  441. startLine: 3,
  442. endLine: 3,
  443. startColumn: 0,
  444. endColumn: 0,
  445. updatedAt: Date.now(),
  446. };
  447. const context = {
  448. ...baseContext,
  449. getNodesByName: (n: string) => (n === 'UsersService' ? [svcNode] : []),
  450. };
  451. const ref = {
  452. fromNodeId: 'class:src/users/users.controller.ts:UsersController:5',
  453. referenceName: 'UsersService',
  454. referenceKind: 'references' as const,
  455. line: 6,
  456. column: 4,
  457. filePath: 'src/users/users.controller.ts',
  458. language: 'typescript' as const,
  459. };
  460. const result = nestjsResolver.resolve(ref, context as any);
  461. expect(result?.targetNodeId).toBe(svcNode.id);
  462. expect(result?.resolvedBy).toBe('framework');
  463. expect(result?.confidence).toBeGreaterThanOrEqual(0.85);
  464. });
  465. it('returns null for a name without a provider suffix', () => {
  466. const ref = {
  467. fromNodeId: 'x',
  468. referenceName: 'doThing',
  469. referenceKind: 'references' as const,
  470. line: 1,
  471. column: 1,
  472. filePath: 'a.ts',
  473. language: 'typescript' as const,
  474. };
  475. expect(nestjsResolver.resolve(ref, baseContext as any)).toBeNull();
  476. });
  477. });
  478. describe('nestjsResolver.postExtract — RouterModule', () => {
  479. function mkClass(name: string, filePath: string, startLine: number, endLine: number): Node {
  480. return {
  481. id: `class:${filePath}:${startLine}:${name}`,
  482. kind: 'class',
  483. name,
  484. qualifiedName: `${filePath}::${name}`,
  485. filePath,
  486. language: 'typescript',
  487. startLine,
  488. endLine,
  489. startColumn: 0,
  490. endColumn: 0,
  491. updatedAt: 0,
  492. };
  493. }
  494. function mkRoute(
  495. filePath: string,
  496. line: number,
  497. method: string,
  498. path: string,
  499. nameOverride?: string
  500. ): Node {
  501. return {
  502. id: `route:${filePath}:${line}:${method}:${path}`,
  503. kind: 'route',
  504. name: nameOverride ?? `${method} ${path}`,
  505. qualifiedName: `${filePath}::${method}:${path}`,
  506. filePath,
  507. language: 'typescript',
  508. startLine: line,
  509. endLine: line,
  510. startColumn: 0,
  511. endColumn: 0,
  512. updatedAt: 0,
  513. };
  514. }
  515. function makeContext(opts: {
  516. files?: Record<string, string>;
  517. nodes?: Node[];
  518. }) {
  519. const files = opts.files ?? {};
  520. const all = opts.nodes ?? [];
  521. return {
  522. getNodesInFile: (fp: string) => all.filter((n) => n.filePath === fp),
  523. getNodesByName: (name: string) => all.filter((n) => n.name === name),
  524. getNodesByQualifiedName: () => [],
  525. getNodesByKind: (kind: Node['kind']) => all.filter((n) => n.kind === kind),
  526. fileExists: (fp: string) => files[fp] !== undefined,
  527. readFile: (fp: string) => files[fp] ?? null,
  528. getProjectRoot: () => '/test',
  529. getAllFiles: () => Object.keys(files),
  530. getNodesByLowerName: () => [],
  531. getImportMappings: () => [],
  532. } as any;
  533. }
  534. it('prepends RouterModule prefix to a controller route (top-level register)', () => {
  535. const ctx = makeContext({
  536. files: {
  537. 'src/app.module.ts': `
  538. @Module({
  539. imports: [
  540. RouterModule.register([
  541. { path: 'admin', module: AdminModule },
  542. ]),
  543. ],
  544. })
  545. export class AppModule {}
  546. @Module({ controllers: [AdminController] })
  547. export class AdminModule {}
  548. `,
  549. },
  550. nodes: [
  551. mkClass('AdminController', 'src/admin/admin.controller.ts', 1, 10),
  552. mkRoute('src/admin/admin.controller.ts', 3, 'GET', '/'),
  553. ],
  554. });
  555. const updates = nestjsResolver.postExtract!(ctx);
  556. expect(updates).toHaveLength(1);
  557. expect(updates[0]!.name).toBe('GET /admin');
  558. // id and qualifiedName must be preserved so existing route→handler edges
  559. // stay intact and the pass remains idempotent on a second run.
  560. expect(updates[0]!.id).toBe('route:src/admin/admin.controller.ts:3:GET:/');
  561. expect(updates[0]!.qualifiedName).toBe('src/admin/admin.controller.ts::GET:/');
  562. });
  563. it('resolves nested children — the issue #459 example', () => {
  564. const ctx = makeContext({
  565. files: {
  566. 'src/app.module.ts': `
  567. @Module({
  568. imports: [
  569. AdminModule,
  570. UsersModule,
  571. RouterModule.register([
  572. {
  573. path: 'admin',
  574. module: AdminModule,
  575. children: [
  576. { path: 'users', module: UsersModule },
  577. ],
  578. },
  579. ]),
  580. ],
  581. })
  582. export class AppModule {}
  583. `,
  584. 'src/users/users.module.ts': `
  585. @Module({ controllers: [UsersController] })
  586. export class UsersModule {}
  587. `,
  588. },
  589. nodes: [
  590. mkClass('UsersController', 'src/users/users.controller.ts', 1, 10),
  591. mkRoute('src/users/users.controller.ts', 3, 'GET', '/'),
  592. ],
  593. });
  594. const updates = nestjsResolver.postExtract!(ctx);
  595. expect(updates).toHaveLength(1);
  596. expect(updates[0]!.name).toBe('GET /admin/users');
  597. });
  598. it('joins module prefix with a non-empty @Controller path and method params', () => {
  599. const ctx = makeContext({
  600. files: {
  601. 'src/app.module.ts': `
  602. RouterModule.register([{ path: 'admin', module: UsersModule }])
  603. @Module({ controllers: [UsersController] })
  604. export class UsersModule {}
  605. `,
  606. },
  607. nodes: [
  608. mkClass('UsersController', 'src/users.controller.ts', 1, 10),
  609. // Existing extract emitted GET /users/:id from @Controller('users') + @Get(':id')
  610. mkRoute('src/users.controller.ts', 3, 'GET', '/users/:id'),
  611. ],
  612. });
  613. const updates = nestjsResolver.postExtract!(ctx);
  614. expect(updates).toHaveLength(1);
  615. expect(updates[0]!.name).toBe('GET /admin/users/:id');
  616. });
  617. it('is idempotent — a second run returns no updates', () => {
  618. // Simulate the state after one round of postExtract: name is already
  619. // 'GET /admin', but qualifiedName still encodes the original 'GET:/'.
  620. const ctx = makeContext({
  621. files: {
  622. 'src/app.module.ts': `
  623. RouterModule.register([{ path: 'admin', module: UsersModule }])
  624. @Module({ controllers: [UsersController] })
  625. export class UsersModule {}
  626. `,
  627. },
  628. nodes: [
  629. mkClass('UsersController', 'src/users.controller.ts', 1, 10),
  630. mkRoute('src/users.controller.ts', 3, 'GET', '/', 'GET /admin'),
  631. ],
  632. });
  633. const updates = nestjsResolver.postExtract!(ctx);
  634. expect(updates).toHaveLength(0);
  635. });
  636. it('is a no-op when the project does not use RouterModule', () => {
  637. const ctx = makeContext({
  638. files: {
  639. 'src/app.module.ts': `
  640. @Module({ controllers: [UsersController] })
  641. export class AppModule {}
  642. `,
  643. },
  644. nodes: [
  645. mkClass('UsersController', 'src/users.controller.ts', 1, 10),
  646. mkRoute('src/users.controller.ts', 3, 'GET', '/'),
  647. ],
  648. });
  649. const updates = nestjsResolver.postExtract!(ctx);
  650. expect(updates).toHaveLength(0);
  651. });
  652. it('attributes routes to the right controller when one file has two', () => {
  653. // Two controllers in one file, declared in two different modules with
  654. // two different module prefixes. The route's startLine has to match the
  655. // class scope, not just the file path.
  656. const ctx = makeContext({
  657. files: {
  658. 'src/app.module.ts': `
  659. RouterModule.register([
  660. { path: 'p1', module: AModule },
  661. { path: 'p2', module: BModule },
  662. ])
  663. @Module({ controllers: [AController] }) export class AModule {}
  664. @Module({ controllers: [BController] }) export class BModule {}
  665. `,
  666. },
  667. nodes: [
  668. mkClass('AController', 'src/multi.controller.ts', 1, 5),
  669. mkClass('BController', 'src/multi.controller.ts', 7, 12),
  670. mkRoute('src/multi.controller.ts', 3, 'GET', '/a/x'),
  671. mkRoute('src/multi.controller.ts', 9, 'GET', '/b/y'),
  672. ],
  673. });
  674. const updates = nestjsResolver.postExtract!(ctx);
  675. expect(updates).toHaveLength(2);
  676. const byId = new Map(updates.map((u) => [u.id, u.name]));
  677. expect(byId.get('route:src/multi.controller.ts:3:GET:/a/x')).toBe('GET /p1/a/x');
  678. expect(byId.get('route:src/multi.controller.ts:9:GET:/b/y')).toBe('GET /p2/b/y');
  679. });
  680. it('merges RouterModule registrations spread across multiple module files', () => {
  681. const ctx = makeContext({
  682. files: {
  683. 'src/app.module.ts': `
  684. RouterModule.register([{ path: 'a', module: AModule }])
  685. @Module({ controllers: [AController] }) export class AModule {}
  686. `,
  687. 'src/feature.module.ts': `
  688. RouterModule.forChild([{ path: 'b', module: BModule }])
  689. @Module({ controllers: [BController] }) export class BModule {}
  690. `,
  691. },
  692. nodes: [
  693. mkClass('AController', 'src/a.controller.ts', 1, 5),
  694. mkClass('BController', 'src/b.controller.ts', 1, 5),
  695. mkRoute('src/a.controller.ts', 3, 'GET', '/'),
  696. mkRoute('src/b.controller.ts', 3, 'GET', '/'),
  697. ],
  698. });
  699. const updates = nestjsResolver.postExtract!(ctx);
  700. expect(updates).toHaveLength(2);
  701. const byId = new Map(updates.map((u) => [u.id, u.name]));
  702. expect(byId.get('route:src/a.controller.ts:3:GET:/')).toBe('GET /a');
  703. expect(byId.get('route:src/b.controller.ts:3:GET:/')).toBe('GET /b');
  704. });
  705. it('silently skips controllers whose class node is not in the graph', () => {
  706. // RouterModule declares a prefix for a module, but the @Module that
  707. // would link it to a controller is missing — common during partial
  708. // re-extraction. Must not throw.
  709. const ctx = makeContext({
  710. files: {
  711. 'src/app.module.ts': `
  712. RouterModule.register([{ path: 'orphans', module: GhostModule }])
  713. @Module({ controllers: [GhostController] }) export class GhostModule {}
  714. `,
  715. },
  716. nodes: [], // no class or route nodes
  717. });
  718. const updates = nestjsResolver.postExtract!(ctx);
  719. expect(updates).toHaveLength(0);
  720. });
  721. });
  722. import { laravelResolver } from '../src/resolution/frameworks/laravel';
  723. describe('laravelResolver.extract', () => {
  724. it('extracts route with controller tuple syntax', () => {
  725. const src = `Route::get('/users', [UserController::class, 'index']);\n`;
  726. const { nodes, references } = laravelResolver.extract!('routes/web.php', src);
  727. expect(nodes[0].name).toBe('GET /users');
  728. expect(references[0].referenceName).toBe('UserController@index');
  729. });
  730. it('extracts route with Controller@action syntax', () => {
  731. const src = `Route::post('/users', 'UserController@store');\n`;
  732. const { nodes, references } = laravelResolver.extract!('routes/web.php', src);
  733. expect(references[0].referenceName).toBe('UserController@store');
  734. });
  735. it('extracts resource route', () => {
  736. const src = `Route::resource('users', UserController::class);\n`;
  737. const { nodes, references } = laravelResolver.extract!('routes/web.php', src);
  738. expect(nodes[0].kind).toBe('route');
  739. expect(references[0].referenceName).toBe('UserController');
  740. });
  741. });
  742. import { railsResolver } from '../src/resolution/frameworks/ruby';
  743. describe('railsResolver.extract', () => {
  744. it('extracts route with controller#action syntax', () => {
  745. const src = `get '/users', to: 'users#index'\n`;
  746. const { nodes, references } = railsResolver.extract!('config/routes.rb', src);
  747. expect(nodes[0].name).toBe('GET /users');
  748. expect(references[0].referenceName).toBe('users#index');
  749. });
  750. it('extracts route without to: keyword', () => {
  751. const src = `post '/items' => 'items#create'\n`;
  752. const { nodes, references } = railsResolver.extract!('config/routes.rb', src);
  753. expect(references[0].referenceName).toBe('items#create');
  754. });
  755. });
  756. import { springResolver } from '../src/resolution/frameworks/java';
  757. describe('springResolver.extract', () => {
  758. it('extracts route with @GetMapping and next method', () => {
  759. const src = `
  760. @GetMapping("/users")
  761. public List<User> listUsers() {
  762. return users;
  763. }
  764. `;
  765. const { nodes, references } = springResolver.extract!('UserController.java', src);
  766. expect(nodes[0].name).toBe('GET /users');
  767. expect(references[0].referenceName).toBe('listUsers');
  768. });
  769. it('extracts a Kotlin @GetMapping with a fun handler', () => {
  770. const src = `
  771. @GetMapping("/vets")
  772. fun showVetList(model: MutableMap<String, Any>): String {
  773. return "vets"
  774. }
  775. `;
  776. const { nodes, references } = springResolver.extract!('VetController.kt', src);
  777. expect(nodes[0].name).toBe('GET /vets');
  778. expect(references[0].referenceName).toBe('showVetList');
  779. expect(nodes[0].language).toBe('kotlin');
  780. });
  781. it('joins a Kotlin class @RequestMapping prefix and skips a stacked annotation', () => {
  782. const src = `
  783. @RestController
  784. @RequestMapping("/owners")
  785. class OwnerController {
  786. @GetMapping("/{ownerId}")
  787. @ResponseBody
  788. fun showOwner(@PathVariable ownerId: Int): String {
  789. return "owner"
  790. }
  791. }
  792. `;
  793. const { nodes, references } = springResolver.extract!('OwnerController.kt', src);
  794. expect(nodes[0].name).toBe('GET /owners/{ownerId}');
  795. expect(references[0].referenceName).toBe('showOwner');
  796. });
  797. });
  798. import { playResolver } from '../src/resolution/frameworks/play';
  799. import { isSourceFile, isPlayRoutesFile } from '../src/extraction/grammars';
  800. describe('playResolver.extract (conf/routes)', () => {
  801. it('extracts METHOD /path Controller.action routes, dropping the package + args', () => {
  802. const src = `# Routes
  803. GET / controllers.Application.index
  804. GET /computers controllers.Application.list(p: Int ?= 0, s: Int ?= 2)
  805. POST /computers controllers.Application.save
  806. -> /v1/posts v1.post.PostRouter
  807. `;
  808. const { nodes, references } = playResolver.extract!('conf/routes', src);
  809. expect(nodes.map((n) => n.name)).toEqual([
  810. 'GET /',
  811. 'GET /computers',
  812. 'POST /computers',
  813. ]); // the `->` include is skipped
  814. expect(references.map((r) => r.referenceName)).toEqual([
  815. 'Application.index',
  816. 'Application.list',
  817. 'Application.save',
  818. ]);
  819. });
  820. it('only runs on Play routes files', () => {
  821. expect(playResolver.extract!('app/Foo.scala', 'GET / controllers.X.y').nodes).toHaveLength(0);
  822. });
  823. });
  824. describe('Play routes file detection', () => {
  825. it('recognizes conf/routes (extensionless) and *.routes as source files', () => {
  826. expect(isPlayRoutesFile('conf/routes')).toBe(true);
  827. expect(isPlayRoutesFile('myapp/conf/routes')).toBe(true);
  828. expect(isPlayRoutesFile('conf/admin.routes')).toBe(true);
  829. expect(isSourceFile('conf/routes')).toBe(true);
  830. expect(isPlayRoutesFile('src/routes.ts')).toBe(false);
  831. });
  832. });
  833. import { goResolver } from '../src/resolution/frameworks/go';
  834. describe('goResolver.extract', () => {
  835. it('extracts route from r.GET', () => {
  836. const src = `r.GET("/users", listUsers)\n`;
  837. const { nodes, references } = goResolver.extract!('main.go', src);
  838. expect(nodes[0].name).toBe('GET /users');
  839. expect(references[0].referenceName).toBe('listUsers');
  840. });
  841. it('extracts route from router.HandleFunc', () => {
  842. const src = `router.HandleFunc("/items", createItem)\n`;
  843. const { nodes, references } = goResolver.extract!('main.go', src);
  844. expect(references[0].referenceName).toBe('createItem');
  845. });
  846. it('extracts gorilla/mux HandleFunc on a subrouter var, ignoring chained .Methods()', () => {
  847. // `s` is a PathPrefix().Subrouter() var — any receiver is matched; the
  848. // trailing .Methods("GET") doesn't break the handler capture.
  849. const src = `s.HandleFunc("/users/{id}", listUsers).Methods("GET")\n`;
  850. const { references } = goResolver.extract!('routes.go', src);
  851. expect(references[0].referenceName).toBe('listUsers');
  852. });
  853. it('does NOT treat verb-named method calls with non-path args as routes (#1259)', () => {
  854. // The issue's repro: a generic cache type whose Put/Get share router verb
  855. // names. First args are keys, not URL paths — no route nodes.
  856. const src = [
  857. `c.Put("a", 1)`,
  858. `c.Put("user:123", value)`,
  859. `store.Get("config", out)`,
  860. `bus.Handle("user.created", onUserCreated)`,
  861. `m.HandleFunc("shutdown", hook)`,
  862. ].join('\n');
  863. const { nodes } = goResolver.extract!('cache.go', src);
  864. expect(nodes).toHaveLength(0);
  865. });
  866. it('keeps real registrations whose paths start with "/" for every router style', () => {
  867. const src = [
  868. `r.Put("/users/{id}", updateUser)`, // chi
  869. `v1.GET("/ping", ping)`, // gin group
  870. `mux.HandleFunc("/healthz", health)`, // net/http
  871. ].join('\n');
  872. const { nodes } = goResolver.extract!('routes.go', src);
  873. expect(nodes.map((n) => n.name)).toEqual(['PUT /users/{id}', 'GET /ping', 'ANY /healthz']);
  874. });
  875. it('recognizes Go 1.22 "METHOD /path" patterns on HandleFunc and extracts the method', () => {
  876. const src = `mux.HandleFunc("GET /api/users/{id}", getUser)\n`;
  877. const { nodes, references } = goResolver.extract!('main.go', src);
  878. expect(nodes[0].name).toBe('GET /api/users/{id}');
  879. expect(references[0].referenceName).toBe('getUser');
  880. });
  881. });
  882. import { goframeResolver } from '../src/resolution/frameworks/goframe';
  883. describe('goframeResolver', () => {
  884. it('detects GoFrame from a gogf/gf dependency in go.mod', () => {
  885. const ctx: any = {
  886. readFile: (f: string) =>
  887. f === 'go.mod' ? 'module example.com/app\nrequire github.com/gogf/gf/v2 v2.7.0\n' : null,
  888. };
  889. expect(goframeResolver.detect(ctx)).toBe(true);
  890. const noGf: any = { readFile: (f: string) => (f === 'go.mod' ? 'module example.com/app\n' : null) };
  891. expect(goframeResolver.detect(noGf)).toBe(false);
  892. });
  893. it('extracts a route node from a g.Meta request struct (method upper-cased)', () => {
  894. const src = `package v1
  895. import "github.com/gogf/gf/v2/frame/g"
  896. type SignInReq struct {
  897. g.Meta \`path:"/user/sign-in" method:"post" tags:"User" summary:"Sign in"\`
  898. Passport string
  899. }
  900. type SignInRes struct{}
  901. `;
  902. const { nodes } = goframeResolver.extract!('api/user/v1/user_sign_in.go', src);
  903. expect(nodes).toHaveLength(1);
  904. expect(nodes[0].kind).toBe('route');
  905. expect(nodes[0].name).toBe('POST /user/sign-in');
  906. // The package-qualified request type is encoded for the synthesizer join.
  907. expect(nodes[0].qualifiedName).toContain('::goframe-route:v1.SignInReq');
  908. });
  909. it('is independent of g.Meta tag attribute order', () => {
  910. const src = `type DeptSearchReq struct {
  911. g.Meta \`path:"/dept/list" tags:"Dept" method:"get" summary:"列表"\`
  912. }`;
  913. const { nodes } = goframeResolver.extract!('api/system/dept.go', src);
  914. expect(nodes[0].name).toBe('GET /dept/list');
  915. expect(nodes[0].qualifiedName).toContain('::goframe-route:DeptSearchReq');
  916. });
  917. it('skips a response g.Meta that has no path (mime-only) and other non-route metadata', () => {
  918. const src = `type ListRes struct {
  919. g.Meta \`mime:"application/json"\`
  920. Items []string
  921. }`;
  922. const { nodes } = goframeResolver.extract!('api/x.go', src);
  923. expect(nodes).toHaveLength(0);
  924. });
  925. it('defaults method to ANY when method: is omitted', () => {
  926. const src = `type PingReq struct {
  927. g.Meta \`path:"/ping"\`
  928. }`;
  929. const { nodes } = goframeResolver.extract!('api/ping.go', src);
  930. expect(nodes[0].name).toBe('ANY /ping');
  931. });
  932. it('extracts every request struct in a multi-route api file', () => {
  933. const src = `type DeptListReq struct { g.Meta \`path:"/dept/list" method:"get"\` }
  934. type DeptListRes struct { g.Meta \`mime:"application/json"\` }
  935. type DeptAddReq struct { g.Meta \`path:"/dept/add" method:"post"\` }
  936. type DeptAddRes struct {}
  937. `;
  938. const { nodes } = goframeResolver.extract!('api/dept.go', src);
  939. expect(nodes.map((n) => n.name).sort()).toEqual(['GET /dept/list', 'POST /dept/add']);
  940. });
  941. it('returns nothing for a non-go file or a file without g.Meta', () => {
  942. expect(goframeResolver.extract!('main.ts', 'const x = 1').nodes).toHaveLength(0);
  943. expect(goframeResolver.extract!('main.go', 'package main\nfunc main() {}\n').nodes).toHaveLength(0);
  944. });
  945. });
  946. import { rustResolver } from '../src/resolution/frameworks/rust';
  947. describe('rustResolver.extract', () => {
  948. it('extracts route from axum .route with get()', () => {
  949. const src = `let app = Router::new().route("/users", get(list_users));\n`;
  950. const { nodes, references } = rustResolver.extract!('main.rs', src);
  951. expect(nodes[0].name).toBe('GET /users');
  952. expect(references[0].referenceName).toBe('list_users');
  953. });
  954. it('extracts every method from a chained axum .route (get().put())', () => {
  955. const src = `let app = Router::new().route("/user", get(get_current_user).put(update_user));\n`;
  956. const { nodes, references } = rustResolver.extract!('main.rs', src);
  957. expect(nodes.map((n) => n.name)).toEqual(['GET /user', 'PUT /user']);
  958. expect(references.map((r) => r.referenceName)).toEqual([
  959. 'get_current_user',
  960. 'update_user',
  961. ]);
  962. });
  963. it('extracts a multi-line axum .route with a namespaced handler', () => {
  964. const src = `
  965. let app = Router::new()
  966. .route(
  967. "/articles/feed",
  968. get(listing::feed_articles),
  969. );
  970. `;
  971. const { nodes, references } = rustResolver.extract!('main.rs', src);
  972. expect(nodes[0].name).toBe('GET /articles/feed');
  973. expect(references[0].referenceName).toBe('feed_articles');
  974. });
  975. it('extracts actix web::resource().route(web::METHOD().to(handler))', () => {
  976. const src = `App::new().service(web::resource("/user/{id}").route(web::get().to(get_user)))\n`;
  977. const { nodes, references } = rustResolver.extract!('main.rs', src);
  978. expect(nodes[0].name).toBe('GET /user/{id}');
  979. expect(references[0].referenceName).toBe('get_user');
  980. });
  981. it('extracts actix web::resource("/").to(handler) (all methods)', () => {
  982. const src = `App::new().service(web::resource("/").to(index))\n`;
  983. const { nodes, references } = rustResolver.extract!('main.rs', src);
  984. expect(nodes[0].name).toBe('ANY /');
  985. expect(references[0].referenceName).toBe('index');
  986. });
  987. it('extracts actix App-level .route("/path", web::METHOD().to(handler))', () => {
  988. const src = `App::new().route("/health", web::get().to(health_check))\n`;
  989. const { nodes, references } = rustResolver.extract!('main.rs', src);
  990. expect(nodes[0].name).toBe('GET /health');
  991. expect(references[0].referenceName).toBe('health_check');
  992. });
  993. });
  994. describe('rustResolver.resolve cargo workspace crates', () => {
  995. it('resolves crate name from workspace member lib.rs', () => {
  996. const workspaceCargo = `
  997. [workspace]
  998. members = ["crates/mytool-core", "crates/mytool-fetcher"]
  999. `;
  1000. const coreCargo = `
  1001. [package]
  1002. name = "mytool-core"
  1003. version = "0.1.0"
  1004. `;
  1005. const libNode: Node = {
  1006. id: 'module:crates/mytool-core/src/lib.rs:mytool_core:1',
  1007. kind: 'module',
  1008. name: 'mytool_core',
  1009. qualifiedName: 'crates/mytool-core/src/lib.rs::mytool_core',
  1010. filePath: 'crates/mytool-core/src/lib.rs',
  1011. language: 'rust',
  1012. startLine: 1,
  1013. endLine: 1,
  1014. startColumn: 0,
  1015. endColumn: 0,
  1016. updatedAt: Date.now(),
  1017. };
  1018. const context = {
  1019. getNodesInFile: (fp: string) => (fp === 'crates/mytool-core/src/lib.rs' ? [libNode] : []),
  1020. getNodesByName: () => [],
  1021. getNodesByQualifiedName: () => [],
  1022. getNodesByKind: () => [],
  1023. fileExists: (p: string) => (
  1024. p === 'Cargo.toml' ||
  1025. p === 'crates/mytool-core/Cargo.toml' ||
  1026. p === 'crates/mytool-core/src/lib.rs'
  1027. ),
  1028. readFile: (p: string) => {
  1029. if (p === 'Cargo.toml') return workspaceCargo;
  1030. if (p === 'crates/mytool-core/Cargo.toml') return coreCargo;
  1031. return null;
  1032. },
  1033. getProjectRoot: () => '/test',
  1034. getAllFiles: () => [
  1035. 'Cargo.toml',
  1036. 'crates/mytool-core/Cargo.toml',
  1037. 'crates/mytool-core/src/lib.rs',
  1038. ],
  1039. getNodesByLowerName: () => [],
  1040. getImportMappings: () => [],
  1041. };
  1042. const ref = {
  1043. fromNodeId: 'fn:crates/mytool-fetcher/src/main.rs:main:1',
  1044. referenceName: 'mytool_core',
  1045. referenceKind: 'references' as const,
  1046. line: 1,
  1047. column: 1,
  1048. filePath: 'crates/mytool-fetcher/src/main.rs',
  1049. language: 'rust' as const,
  1050. };
  1051. const result = rustResolver.resolve(ref, context);
  1052. expect(result?.targetNodeId).toBe(libNode.id);
  1053. expect(result?.resolvedBy).toBe('framework');
  1054. // Workspace-manifest hits are unambiguous and must beat name-matcher's
  1055. // self-file matches (0.7) so cross-crate `imports` edges materialize.
  1056. expect(result?.confidence).toBeGreaterThanOrEqual(0.9);
  1057. });
  1058. it('resolves crate name from workspace member main.rs when lib.rs is absent', () => {
  1059. const workspaceCargo = `
  1060. [workspace]
  1061. members = [
  1062. "crates/mytool-runner",
  1063. ]
  1064. `;
  1065. const runnerCargo = `
  1066. [package]
  1067. name = "mytool-runner"
  1068. version = "0.1.0"
  1069. `;
  1070. const mainNode: Node = {
  1071. id: 'module:crates/mytool-runner/src/main.rs:mytool_runner:1',
  1072. kind: 'module',
  1073. name: 'mytool_runner',
  1074. qualifiedName: 'crates/mytool-runner/src/main.rs::mytool_runner',
  1075. filePath: 'crates/mytool-runner/src/main.rs',
  1076. language: 'rust',
  1077. startLine: 1,
  1078. endLine: 1,
  1079. startColumn: 0,
  1080. endColumn: 0,
  1081. updatedAt: Date.now(),
  1082. };
  1083. const context = {
  1084. getNodesInFile: (fp: string) => (fp === 'crates/mytool-runner/src/main.rs' ? [mainNode] : []),
  1085. getNodesByName: () => [],
  1086. getNodesByQualifiedName: () => [],
  1087. getNodesByKind: () => [],
  1088. fileExists: (p: string) => (
  1089. p === 'Cargo.toml' ||
  1090. p === 'crates/mytool-runner/Cargo.toml' ||
  1091. p === 'crates/mytool-runner/src/main.rs'
  1092. ),
  1093. readFile: (p: string) => {
  1094. if (p === 'Cargo.toml') return workspaceCargo;
  1095. if (p === 'crates/mytool-runner/Cargo.toml') return runnerCargo;
  1096. return null;
  1097. },
  1098. getProjectRoot: () => '/test',
  1099. getAllFiles: () => [
  1100. 'Cargo.toml',
  1101. 'crates/mytool-runner/Cargo.toml',
  1102. 'crates/mytool-runner/src/main.rs',
  1103. ],
  1104. getNodesByLowerName: () => [],
  1105. getImportMappings: () => [],
  1106. };
  1107. const ref = {
  1108. fromNodeId: 'fn:crates/mytool-runner/src/main.rs:main:1',
  1109. referenceName: 'mytool_runner',
  1110. referenceKind: 'references' as const,
  1111. line: 1,
  1112. column: 1,
  1113. filePath: 'crates/mytool-runner/src/main.rs',
  1114. language: 'rust' as const,
  1115. };
  1116. const result = rustResolver.resolve(ref, context);
  1117. expect(result?.targetNodeId).toBe(mainNode.id);
  1118. expect(result?.resolvedBy).toBe('framework');
  1119. });
  1120. it('resolves crate name when members uses a glob (crates/*)', () => {
  1121. const workspaceCargo = `
  1122. [workspace]
  1123. members = ["crates/*"]
  1124. `;
  1125. const fooCargo = `
  1126. [package]
  1127. name = "mytool-foo"
  1128. version = "0.1.0"
  1129. `;
  1130. const barCargo = `
  1131. [package]
  1132. name = "mytool-bar"
  1133. version = "0.1.0"
  1134. `;
  1135. const fooLib: Node = {
  1136. id: 'module:crates/mytool-foo/src/lib.rs:mytool_foo:1',
  1137. kind: 'module',
  1138. name: 'mytool_foo',
  1139. qualifiedName: 'crates/mytool-foo/src/lib.rs::mytool_foo',
  1140. filePath: 'crates/mytool-foo/src/lib.rs',
  1141. language: 'rust',
  1142. startLine: 1,
  1143. endLine: 1,
  1144. startColumn: 0,
  1145. endColumn: 0,
  1146. updatedAt: Date.now(),
  1147. };
  1148. const barLib: Node = {
  1149. id: 'module:crates/mytool-bar/src/lib.rs:mytool_bar:1',
  1150. kind: 'module',
  1151. name: 'mytool_bar',
  1152. qualifiedName: 'crates/mytool-bar/src/lib.rs::mytool_bar',
  1153. filePath: 'crates/mytool-bar/src/lib.rs',
  1154. language: 'rust',
  1155. startLine: 1,
  1156. endLine: 1,
  1157. startColumn: 0,
  1158. endColumn: 0,
  1159. updatedAt: Date.now(),
  1160. };
  1161. const filesByPath: Record<string, string> = {
  1162. 'Cargo.toml': workspaceCargo,
  1163. 'crates/mytool-foo/Cargo.toml': fooCargo,
  1164. 'crates/mytool-bar/Cargo.toml': barCargo,
  1165. };
  1166. const nodesByFile: Record<string, Node[]> = {
  1167. 'crates/mytool-foo/src/lib.rs': [fooLib],
  1168. 'crates/mytool-bar/src/lib.rs': [barLib],
  1169. };
  1170. const dirsByPath: Record<string, string[]> = {
  1171. '.': ['crates'],
  1172. crates: ['mytool-foo', 'mytool-bar'],
  1173. 'crates/mytool-foo': ['src'],
  1174. 'crates/mytool-bar': ['src'],
  1175. };
  1176. const context = {
  1177. getNodesInFile: (fp: string) => nodesByFile[fp] ?? [],
  1178. getNodesByName: () => [],
  1179. getNodesByQualifiedName: () => [],
  1180. getNodesByKind: () => [],
  1181. fileExists: (p: string) => (
  1182. Object.prototype.hasOwnProperty.call(filesByPath, p) ||
  1183. Object.prototype.hasOwnProperty.call(nodesByFile, p)
  1184. ),
  1185. readFile: (p: string) => filesByPath[p] ?? null,
  1186. getProjectRoot: () => '/test',
  1187. getAllFiles: () => [
  1188. 'Cargo.toml',
  1189. ...Object.keys(filesByPath).filter((p) => p !== 'Cargo.toml'),
  1190. ...Object.keys(nodesByFile),
  1191. ],
  1192. getNodesByLowerName: () => [],
  1193. getImportMappings: () => [],
  1194. listDirectories: (rel: string) => dirsByPath[rel] ?? [],
  1195. };
  1196. const fooRef = {
  1197. fromNodeId: 'fn:crates/mytool-bar/src/lib.rs:other:1',
  1198. referenceName: 'mytool_foo',
  1199. referenceKind: 'references' as const,
  1200. line: 1,
  1201. column: 1,
  1202. filePath: 'crates/mytool-bar/src/lib.rs',
  1203. language: 'rust' as const,
  1204. };
  1205. const barRef = {
  1206. fromNodeId: 'fn:crates/mytool-foo/src/lib.rs:other:1',
  1207. referenceName: 'mytool_bar',
  1208. referenceKind: 'references' as const,
  1209. line: 1,
  1210. column: 1,
  1211. filePath: 'crates/mytool-foo/src/lib.rs',
  1212. language: 'rust' as const,
  1213. };
  1214. expect(rustResolver.resolve(fooRef, context)?.targetNodeId).toBe(fooLib.id);
  1215. expect(rustResolver.resolve(barRef, context)?.targetNodeId).toBe(barLib.id);
  1216. });
  1217. it('resolves crate name when members uses a name glob at root (helix-*)', () => {
  1218. const workspaceCargo = `
  1219. [workspace]
  1220. members = ["helix-*"]
  1221. `;
  1222. const coreCargo = `
  1223. [package]
  1224. name = "helix-core"
  1225. version = "0.1.0"
  1226. `;
  1227. const coreLib: Node = {
  1228. id: 'module:helix-core/src/lib.rs:helix_core:1',
  1229. kind: 'module',
  1230. name: 'helix_core',
  1231. qualifiedName: 'helix-core/src/lib.rs::helix_core',
  1232. filePath: 'helix-core/src/lib.rs',
  1233. language: 'rust',
  1234. startLine: 1,
  1235. endLine: 1,
  1236. startColumn: 0,
  1237. endColumn: 0,
  1238. updatedAt: Date.now(),
  1239. };
  1240. const filesByPath: Record<string, string> = {
  1241. 'Cargo.toml': workspaceCargo,
  1242. 'helix-core/Cargo.toml': coreCargo,
  1243. };
  1244. const nodesByFile: Record<string, Node[]> = {
  1245. 'helix-core/src/lib.rs': [coreLib],
  1246. };
  1247. const dirsByPath: Record<string, string[]> = {
  1248. '.': ['helix-core', 'docs', 'target'],
  1249. 'helix-core': ['src'],
  1250. };
  1251. const context = {
  1252. getNodesInFile: (fp: string) => nodesByFile[fp] ?? [],
  1253. getNodesByName: () => [],
  1254. getNodesByQualifiedName: () => [],
  1255. getNodesByKind: () => [],
  1256. fileExists: (p: string) => (
  1257. Object.prototype.hasOwnProperty.call(filesByPath, p) ||
  1258. Object.prototype.hasOwnProperty.call(nodesByFile, p)
  1259. ),
  1260. readFile: (p: string) => filesByPath[p] ?? null,
  1261. getProjectRoot: () => '/test',
  1262. getAllFiles: () => [
  1263. 'Cargo.toml',
  1264. ...Object.keys(filesByPath).filter((p) => p !== 'Cargo.toml'),
  1265. ...Object.keys(nodesByFile),
  1266. ],
  1267. getNodesByLowerName: () => [],
  1268. getImportMappings: () => [],
  1269. listDirectories: (rel: string) => dirsByPath[rel] ?? [],
  1270. };
  1271. const ref = {
  1272. fromNodeId: 'fn:helix-core/src/lib.rs:other:1',
  1273. referenceName: 'helix_core',
  1274. referenceKind: 'references' as const,
  1275. line: 1,
  1276. column: 1,
  1277. filePath: 'helix-core/src/lib.rs',
  1278. language: 'rust' as const,
  1279. };
  1280. expect(rustResolver.resolve(ref, context)?.targetNodeId).toBe(coreLib.id);
  1281. });
  1282. });
  1283. import { aspnetResolver } from '../src/resolution/frameworks/csharp';
  1284. describe('aspnetResolver.extract', () => {
  1285. it('extracts route from [HttpGet] attribute', () => {
  1286. const src = `
  1287. [HttpGet("/users")]
  1288. public IActionResult ListUsers()
  1289. {
  1290. return Ok();
  1291. }
  1292. `;
  1293. const { nodes, references } = aspnetResolver.extract!('UserController.cs', src);
  1294. expect(nodes[0].name).toBe('GET /users');
  1295. expect(references[0].referenceName).toBe('ListUsers');
  1296. });
  1297. });
  1298. import { vaporResolver } from '../src/resolution/frameworks/swift';
  1299. describe('vaporResolver.extract', () => {
  1300. it('extracts route from app.get with use:', () => {
  1301. const src = `app.get("users", use: listUsers)\n`;
  1302. const { nodes, references } = vaporResolver.extract!('routes.swift', src);
  1303. expect(nodes[0].name).toBe('GET /users');
  1304. expect(references[0].referenceName).toBe('listUsers');
  1305. });
  1306. it('extracts grouped RouteCollection routes with the group prefix and no path arg', () => {
  1307. const src = `
  1308. func boot(routes: RoutesBuilder) throws {
  1309. let todos = routes.grouped("todos")
  1310. todos.get(use: index)
  1311. todos.post(use: create)
  1312. todos.group(":todoID") { todo in
  1313. todo.delete(use: delete)
  1314. }
  1315. }
  1316. `;
  1317. const { nodes, references } = vaporResolver.extract!('TodoController.swift', src);
  1318. expect(nodes.map((n) => n.name).sort()).toEqual([
  1319. 'DELETE /todos/:todoID',
  1320. 'GET /todos',
  1321. 'POST /todos',
  1322. ]);
  1323. expect(references.map((r) => r.referenceName).sort()).toEqual([
  1324. 'create',
  1325. 'delete',
  1326. 'index',
  1327. ]);
  1328. });
  1329. it('handles use: self.handler and non-string path segments', () => {
  1330. const src = `router.get("users", User.parameter, "edit", use: self.editUserHandler)\n`;
  1331. const { nodes, references } = vaporResolver.extract!('UserController.swift', src);
  1332. expect(nodes[0].name).toBe('GET /users/edit');
  1333. expect(references[0].referenceName).toBe('editUserHandler');
  1334. });
  1335. it('ignores non-route .get calls that lack use: (e.g. Environment.get)', () => {
  1336. const src = `let host = Environment.get("DATABASE_HOST") ?? "localhost"\n`;
  1337. const { nodes } = vaporResolver.extract!('configure.swift', src);
  1338. expect(nodes).toHaveLength(0);
  1339. });
  1340. });
  1341. import { reactResolver } from '../src/resolution/frameworks/react';
  1342. import { svelteResolver } from '../src/resolution/frameworks/svelte';
  1343. import { astroResolver } from '../src/resolution/frameworks/astro';
  1344. describe('reactResolver.extract — React Router', () => {
  1345. it('extracts a v6 <Route path element={<Comp/>}>', () => {
  1346. const src = `<Route path="/users" element={<UsersPage/>}/>`;
  1347. const { nodes, references } = reactResolver.extract!('App.tsx', src);
  1348. const route = nodes.find((n) => n.kind === 'route');
  1349. expect(route?.name).toBe('/users');
  1350. expect(references[0]?.referenceName).toBe('UsersPage');
  1351. });
  1352. it('extracts a v5 <Route path component={Comp}> with attributes in any order', () => {
  1353. const src = `<Route exact path="/login" component={Login} />`;
  1354. const { nodes, references } = reactResolver.extract!('App.jsx', src);
  1355. const route = nodes.find((n) => n.kind === 'route');
  1356. expect(route?.name).toBe('/login');
  1357. expect(references[0]?.referenceName).toBe('Login');
  1358. });
  1359. it('does not treat the <Routes> container as a route', () => {
  1360. const src = `<Routes><Route path="/x" element={<X/>}/></Routes>`;
  1361. const routes = reactResolver.extract!('App.tsx', src).nodes.filter((n) => n.kind === 'route');
  1362. expect(routes).toHaveLength(1);
  1363. expect(routes[0]?.name).toBe('/x');
  1364. });
  1365. it('extracts createBrowserRouter object routes ({ path, element/Component })', () => {
  1366. const src = `const router = createBrowserRouter([
  1367. { path: "/dashboard", element: <Dashboard /> },
  1368. { path: "/login", Component: Login },
  1369. ]);`;
  1370. const { nodes, references } = reactResolver.extract!('router.tsx', src);
  1371. const routes = nodes.filter((n) => n.kind === 'route');
  1372. expect(routes.map((n) => n.name).sort()).toEqual(['/dashboard', '/login']);
  1373. expect(references.map((r) => r.referenceName).sort()).toEqual(['Dashboard', 'Login']);
  1374. });
  1375. it('does not treat config files or a nextjs-pages dir as Next.js routes', () => {
  1376. const cfg = reactResolver.extract!('apps/nextjs-pages/next.config.mjs', 'export default {}');
  1377. expect(cfg.nodes.filter((n) => n.kind === 'route')).toHaveLength(0);
  1378. const vite = reactResolver.extract!('src/pages/vite.config.ts', 'export default {}');
  1379. expect(vite.nodes.filter((n) => n.kind === 'route')).toHaveLength(0);
  1380. // a real page still works
  1381. const page = reactResolver.extract!('src/pages/about.tsx', 'export default function About(){return null}');
  1382. expect(page.nodes.filter((n) => n.kind === 'route').map((n) => n.name)).toEqual(['/about']);
  1383. });
  1384. });
  1385. describe('svelteResolver.extract (smoke)', () => {
  1386. it('returns { nodes, references } shape', () => {
  1387. const result = svelteResolver.extract!('+page.svelte', '');
  1388. expect(result).toHaveProperty('nodes');
  1389. expect(result).toHaveProperty('references');
  1390. });
  1391. });
  1392. describe('astroResolver.extract — src/pages file-based routing', () => {
  1393. const routeNames = (filePath: string): string[] =>
  1394. astroResolver.extract!(filePath, '').nodes.filter((n) => n.kind === 'route').map((n) => n.name);
  1395. it('maps index.astro to /', () => {
  1396. expect(routeNames('src/pages/index.astro')).toEqual(['/']);
  1397. });
  1398. it('maps nested index and plain pages', () => {
  1399. expect(routeNames('src/pages/blog/index.astro')).toEqual(['/blog']);
  1400. expect(routeNames('src/pages/about.astro')).toEqual(['/about']);
  1401. });
  1402. it('converts [param] and [...rest] syntax', () => {
  1403. expect(routeNames('src/pages/blog/[slug].astro')).toEqual(['/blog/:slug']);
  1404. expect(routeNames('src/pages/[...path].astro')).toEqual(['/*path']);
  1405. });
  1406. it('maps .ts endpoints under src/pages to routes', () => {
  1407. expect(routeNames('src/pages/api/posts.ts')).toEqual(['/api/posts']);
  1408. expect(routeNames('src/pages/rss.xml.js')).toEqual(['/rss.xml']);
  1409. });
  1410. it('excludes underscore-prefixed segments and config files', () => {
  1411. expect(routeNames('src/pages/_partial.astro')).toEqual([]);
  1412. expect(routeNames('src/pages/blog/_components/Card.astro')).toEqual([]);
  1413. expect(routeNames('src/pages/vite.config.ts')).toEqual([]);
  1414. });
  1415. it('ignores .astro files outside src/pages', () => {
  1416. expect(routeNames('src/components/Button.astro')).toEqual([]);
  1417. expect(routeNames('docs/pages/guide.astro')).toEqual([]);
  1418. });
  1419. });
  1420. describe('astroResolver.resolve — Astro global and virtual modules', () => {
  1421. const ctx = {} as never;
  1422. const baseRef = {
  1423. fromNodeId: 'component:a',
  1424. line: 1,
  1425. column: 0,
  1426. filePath: 'src/pages/index.astro',
  1427. language: 'astro',
  1428. };
  1429. it('claims Astro.* global references as framework-provided', () => {
  1430. const res = astroResolver.resolve(
  1431. { ...baseRef, referenceName: 'Astro.props', referenceKind: 'references' } as never,
  1432. ctx
  1433. );
  1434. expect(res?.resolvedBy).toBe('framework');
  1435. expect(res?.confidence).toBe(1.0);
  1436. });
  1437. it('claims astro:content virtual module imports', () => {
  1438. const res = astroResolver.resolve(
  1439. { ...baseRef, referenceName: 'astro:content', referenceKind: 'imports' } as never,
  1440. ctx
  1441. );
  1442. expect(res?.resolvedBy).toBe('framework');
  1443. });
  1444. it('leaves ordinary names alone', () => {
  1445. const res = astroResolver.resolve(
  1446. { ...baseRef, referenceName: 'astrolabe', referenceKind: 'calls' } as never,
  1447. { getNodesByName: () => [] } as never
  1448. );
  1449. expect(res).toBeNull();
  1450. });
  1451. });
  1452. // Regression tests: commented-out and docstring route examples must NOT
  1453. // surface as phantom route nodes. These would have failed before the
  1454. // strip-comments wiring (the regex would happily scan comments/docstrings).
  1455. describe('framework extractors ignore commented-out routes', () => {
  1456. it('django: skips line-comment and docstring routes', () => {
  1457. const src = `
  1458. # urls.py example:
  1459. # path('/admin/', AdminPanel.as_view())
  1460. """
  1461. Other routing example:
  1462. path('/users/', UserListView.as_view())
  1463. """
  1464. urlpatterns = [path('/real/', RealView.as_view())]
  1465. `;
  1466. const result = djangoResolver.extract!('app/urls.py', src);
  1467. const urls = result.nodes.map((n) => n.name);
  1468. expect(urls).toEqual(['/real/']);
  1469. });
  1470. it('flask: skips commented-out @app.route', () => {
  1471. const src = `
  1472. # @app.route('/fake')
  1473. # def fake_view():
  1474. # return ''
  1475. @app.route('/real')
  1476. def real_view():
  1477. return ''
  1478. `;
  1479. const { nodes, references } = flaskResolver.extract!('app.py', src);
  1480. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1481. expect(references.map((r) => r.referenceName)).toEqual(['real_view']);
  1482. });
  1483. it('fastapi: skips docstring example routes', () => {
  1484. const src = `
  1485. """
  1486. Example:
  1487. @app.get('/in-docstring')
  1488. async def doc():
  1489. pass
  1490. """
  1491. @app.get('/real')
  1492. async def real_handler():
  1493. return {}
  1494. `;
  1495. const { nodes, references } = fastapiResolver.extract!('main.py', src);
  1496. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1497. expect(references.map((r) => r.referenceName)).toEqual(['real_handler']);
  1498. });
  1499. it('express: skips // and /* */ commented routes', () => {
  1500. const src = `
  1501. // app.get('/fake', fakeHandler);
  1502. /* router.post('/also-fake', otherHandler); */
  1503. app.get('/real', realHandler);
  1504. `;
  1505. const { nodes, references } = expressResolver.extract!('routes.ts', src);
  1506. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1507. expect(references.map((r) => r.referenceName)).toEqual(['realHandler']);
  1508. });
  1509. it('laravel: skips // # and /* */ commented Route::* calls', () => {
  1510. const src = `<?php
  1511. // Route::get('/fake', [FakeController::class, 'index']);
  1512. # Route::get('/also-fake', 'FakeController@show');
  1513. /* Route::post('/another-fake', [X::class, 'y']); */
  1514. Route::get('/real', [RealController::class, 'index']);
  1515. `;
  1516. const { nodes, references } = laravelResolver.extract!('routes/web.php', src);
  1517. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1518. expect(references.map((r) => r.referenceName)).toEqual(['RealController@index']);
  1519. });
  1520. it('rails: skips =begin/=end and # commented routes', () => {
  1521. const src = `
  1522. # get '/fake', to: 'fake#index'
  1523. =begin
  1524. get '/also-fake', to: 'fake#show'
  1525. =end
  1526. get '/real', to: 'real#index'
  1527. `;
  1528. const { nodes, references } = railsResolver.extract!('config/routes.rb', src);
  1529. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1530. expect(references.map((r) => r.referenceName)).toEqual(['real#index']);
  1531. });
  1532. it('spring: skips // and /* */ commented @GetMapping', () => {
  1533. const src = `
  1534. // @GetMapping("/fake")
  1535. // public List<X> fake() { return null; }
  1536. /* @PostMapping("/also-fake")
  1537. public void alsoFake() {} */
  1538. @GetMapping("/real")
  1539. public List<User> listUsers() { return users; }
  1540. `;
  1541. const { nodes, references } = springResolver.extract!('UserController.java', src);
  1542. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1543. expect(references.map((r) => r.referenceName)).toEqual(['listUsers']);
  1544. });
  1545. it('go: skips // and /* */ commented router.METHOD calls', () => {
  1546. const src = `
  1547. // r.GET("/fake", fakeHandler)
  1548. /* r.POST("/also-fake", anotherHandler) */
  1549. r.GET("/real", listUsers)
  1550. `;
  1551. const { nodes, references } = goResolver.extract!('main.go', src);
  1552. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1553. expect(references.map((r) => r.referenceName)).toEqual(['listUsers']);
  1554. });
  1555. it('rust: skips // and nested /* */ commented .route() calls', () => {
  1556. const src = `
  1557. // .route("/fake", get(fake_handler))
  1558. /* outer /* inner .route("/inner-fake", get(x)) */ still .route("/outer-fake", get(y)) */
  1559. let app = Router::new().route("/real", get(list_users));
  1560. `;
  1561. const { nodes, references } = rustResolver.extract!('main.rs', src);
  1562. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1563. expect(references.map((r) => r.referenceName)).toEqual(['list_users']);
  1564. });
  1565. it('aspnet: skips // and /* */ commented [HttpGet] attributes', () => {
  1566. const src = `
  1567. // [HttpGet("/fake")]
  1568. // public IActionResult Fake() { return Ok(); }
  1569. /* [HttpPost("/also-fake")]
  1570. public IActionResult AlsoFake() { return Ok(); } */
  1571. [HttpGet("/real")]
  1572. public IActionResult ListUsers() { return Ok(); }
  1573. `;
  1574. const { nodes, references } = aspnetResolver.extract!('UserController.cs', src);
  1575. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1576. expect(references.map((r) => r.referenceName)).toEqual(['ListUsers']);
  1577. });
  1578. it('vapor: skips // and /* */ commented app.METHOD calls', () => {
  1579. const src = `
  1580. // app.get("fake", use: fakeHandler)
  1581. /* app.post("also-fake", use: anotherHandler) */
  1582. app.get("real", use: listUsers)
  1583. `;
  1584. const { nodes, references } = vaporResolver.extract!('routes.swift', src);
  1585. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  1586. expect(references.map((r) => r.referenceName)).toEqual(['listUsers']);
  1587. });
  1588. it('nestjs: skips // and /* */ commented decorators', () => {
  1589. const src = `
  1590. @Controller('users')
  1591. export class UsersController {
  1592. // @Get('fake')
  1593. // fake() {}
  1594. /* @Post('also-fake')
  1595. alsoFake() {} */
  1596. @Get('real')
  1597. real() {}
  1598. }
  1599. `;
  1600. const { nodes, references } = nestjsResolver.extract!('users.controller.ts', src);
  1601. expect(nodes.map((n) => n.name)).toEqual(['GET /users/real']);
  1602. expect(references.map((r) => r.referenceName)).toEqual(['real']);
  1603. });
  1604. });