frameworks.test.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  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. });
  111. describe('fastapiResolver.extract', () => {
  112. it('extracts route and reference from @app.get', () => {
  113. const src = `
  114. @app.get('/users')
  115. async def list_users():
  116. return []
  117. `;
  118. const { nodes, references } = fastapiResolver.extract!('main.py', src);
  119. expect(nodes[0].name).toBe('GET /users');
  120. expect(references[0].referenceName).toBe('list_users');
  121. });
  122. it('extracts route from router.post', () => {
  123. const src = `
  124. @router.post('/items')
  125. def create_item(item: Item):
  126. pass
  127. `;
  128. const { nodes, references } = fastapiResolver.extract!('items.py', src);
  129. expect(nodes[0].name).toBe('POST /items');
  130. expect(references[0].referenceName).toBe('create_item');
  131. });
  132. });
  133. import { expressResolver } from '../src/resolution/frameworks/express';
  134. describe('expressResolver.extract', () => {
  135. it('extracts route with inline handler reference', () => {
  136. const src = `app.get('/users', listUsers);\n`;
  137. const { nodes, references } = expressResolver.extract!('routes.ts', src);
  138. expect(nodes).toHaveLength(1);
  139. expect(nodes[0].name).toBe('GET /users');
  140. expect(references[0].referenceName).toBe('listUsers');
  141. });
  142. it('extracts route with router.post and middleware chain', () => {
  143. const src = `router.post('/items', auth, createItem);\n`;
  144. const { nodes, references } = expressResolver.extract!('items.ts', src);
  145. expect(nodes[0].name).toBe('POST /items');
  146. // Multiple handlers: prefer the LAST one (convention: middleware first, handler last)
  147. expect(references[0].referenceName).toBe('createItem');
  148. });
  149. it('extracts route with controller method reference', () => {
  150. const src = `app.get('/x', userController.list);\n`;
  151. const { nodes, references } = expressResolver.extract!('routes.ts', src);
  152. expect(references[0].referenceName).toBe('list');
  153. });
  154. });
  155. import { laravelResolver } from '../src/resolution/frameworks/laravel';
  156. describe('laravelResolver.extract', () => {
  157. it('extracts route with controller tuple syntax', () => {
  158. const src = `Route::get('/users', [UserController::class, 'index']);\n`;
  159. const { nodes, references } = laravelResolver.extract!('routes/web.php', src);
  160. expect(nodes[0].name).toBe('GET /users');
  161. expect(references[0].referenceName).toBe('index');
  162. });
  163. it('extracts route with Controller@action syntax', () => {
  164. const src = `Route::post('/users', 'UserController@store');\n`;
  165. const { nodes, references } = laravelResolver.extract!('routes/web.php', src);
  166. expect(references[0].referenceName).toBe('store');
  167. });
  168. it('extracts resource route', () => {
  169. const src = `Route::resource('users', UserController::class);\n`;
  170. const { nodes, references } = laravelResolver.extract!('routes/web.php', src);
  171. expect(nodes[0].kind).toBe('route');
  172. expect(references[0].referenceName).toBe('UserController');
  173. });
  174. });
  175. import { railsResolver } from '../src/resolution/frameworks/ruby';
  176. describe('railsResolver.extract', () => {
  177. it('extracts route with controller#action syntax', () => {
  178. const src = `get '/users', to: 'users#index'\n`;
  179. const { nodes, references } = railsResolver.extract!('config/routes.rb', src);
  180. expect(nodes[0].name).toBe('GET /users');
  181. expect(references[0].referenceName).toBe('index');
  182. });
  183. it('extracts route without to: keyword', () => {
  184. const src = `post '/items' => 'items#create'\n`;
  185. const { nodes, references } = railsResolver.extract!('config/routes.rb', src);
  186. expect(references[0].referenceName).toBe('create');
  187. });
  188. });
  189. import { springResolver } from '../src/resolution/frameworks/java';
  190. describe('springResolver.extract', () => {
  191. it('extracts route with @GetMapping and next method', () => {
  192. const src = `
  193. @GetMapping("/users")
  194. public List<User> listUsers() {
  195. return users;
  196. }
  197. `;
  198. const { nodes, references } = springResolver.extract!('UserController.java', src);
  199. expect(nodes[0].name).toBe('GET /users');
  200. expect(references[0].referenceName).toBe('listUsers');
  201. });
  202. });
  203. import { goResolver } from '../src/resolution/frameworks/go';
  204. describe('goResolver.extract', () => {
  205. it('extracts route from r.GET', () => {
  206. const src = `r.GET("/users", listUsers)\n`;
  207. const { nodes, references } = goResolver.extract!('main.go', src);
  208. expect(nodes[0].name).toBe('GET /users');
  209. expect(references[0].referenceName).toBe('listUsers');
  210. });
  211. it('extracts route from router.HandleFunc', () => {
  212. const src = `router.HandleFunc("/items", createItem)\n`;
  213. const { nodes, references } = goResolver.extract!('main.go', src);
  214. expect(references[0].referenceName).toBe('createItem');
  215. });
  216. });
  217. import { rustResolver } from '../src/resolution/frameworks/rust';
  218. describe('rustResolver.extract', () => {
  219. it('extracts route from axum .route with get()', () => {
  220. const src = `let app = Router::new().route("/users", get(list_users));\n`;
  221. const { nodes, references } = rustResolver.extract!('main.rs', src);
  222. expect(nodes[0].name).toBe('GET /users');
  223. expect(references[0].referenceName).toBe('list_users');
  224. });
  225. });
  226. import { aspnetResolver } from '../src/resolution/frameworks/csharp';
  227. describe('aspnetResolver.extract', () => {
  228. it('extracts route from [HttpGet] attribute', () => {
  229. const src = `
  230. [HttpGet("/users")]
  231. public IActionResult ListUsers()
  232. {
  233. return Ok();
  234. }
  235. `;
  236. const { nodes, references } = aspnetResolver.extract!('UserController.cs', src);
  237. expect(nodes[0].name).toBe('GET /users');
  238. expect(references[0].referenceName).toBe('ListUsers');
  239. });
  240. });
  241. import { vaporResolver } from '../src/resolution/frameworks/swift';
  242. describe('vaporResolver.extract', () => {
  243. it('extracts route from app.get with use:', () => {
  244. const src = `app.get("users", use: listUsers)\n`;
  245. const { nodes, references } = vaporResolver.extract!('routes.swift', src);
  246. expect(nodes[0].name).toBe('GET users');
  247. expect(references[0].referenceName).toBe('listUsers');
  248. });
  249. });
  250. import { reactResolver } from '../src/resolution/frameworks/react';
  251. import { svelteResolver } from '../src/resolution/frameworks/svelte';
  252. describe('reactResolver.extract (smoke)', () => {
  253. it('returns { nodes, references } shape', () => {
  254. const src = `<Route path="/users" element={<UsersPage/>}/>`;
  255. const result = reactResolver.extract!('App.tsx', src);
  256. expect(result).toHaveProperty('nodes');
  257. expect(result).toHaveProperty('references');
  258. expect(Array.isArray(result.nodes)).toBe(true);
  259. expect(Array.isArray(result.references)).toBe(true);
  260. });
  261. });
  262. describe('svelteResolver.extract (smoke)', () => {
  263. it('returns { nodes, references } shape', () => {
  264. const result = svelteResolver.extract!('+page.svelte', '');
  265. expect(result).toHaveProperty('nodes');
  266. expect(result).toHaveProperty('references');
  267. });
  268. });
  269. // Regression tests: commented-out and docstring route examples must NOT
  270. // surface as phantom route nodes. These would have failed before the
  271. // strip-comments wiring (the regex would happily scan comments/docstrings).
  272. describe('framework extractors ignore commented-out routes', () => {
  273. it('django: skips line-comment and docstring routes', () => {
  274. const src = `
  275. # urls.py example:
  276. # path('/admin/', AdminPanel.as_view())
  277. """
  278. Other routing example:
  279. path('/users/', UserListView.as_view())
  280. """
  281. urlpatterns = [path('/real/', RealView.as_view())]
  282. `;
  283. const result = djangoResolver.extract!('app/urls.py', src);
  284. const urls = result.nodes.map((n) => n.name);
  285. expect(urls).toEqual(['/real/']);
  286. });
  287. it('flask: skips commented-out @app.route', () => {
  288. const src = `
  289. # @app.route('/fake')
  290. # def fake_view():
  291. # return ''
  292. @app.route('/real')
  293. def real_view():
  294. return ''
  295. `;
  296. const { nodes, references } = flaskResolver.extract!('app.py', src);
  297. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  298. expect(references.map((r) => r.referenceName)).toEqual(['real_view']);
  299. });
  300. it('fastapi: skips docstring example routes', () => {
  301. const src = `
  302. """
  303. Example:
  304. @app.get('/in-docstring')
  305. async def doc():
  306. pass
  307. """
  308. @app.get('/real')
  309. async def real_handler():
  310. return {}
  311. `;
  312. const { nodes, references } = fastapiResolver.extract!('main.py', src);
  313. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  314. expect(references.map((r) => r.referenceName)).toEqual(['real_handler']);
  315. });
  316. it('express: skips // and /* */ commented routes', () => {
  317. const src = `
  318. // app.get('/fake', fakeHandler);
  319. /* router.post('/also-fake', otherHandler); */
  320. app.get('/real', realHandler);
  321. `;
  322. const { nodes, references } = expressResolver.extract!('routes.ts', src);
  323. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  324. expect(references.map((r) => r.referenceName)).toEqual(['realHandler']);
  325. });
  326. it('laravel: skips // # and /* */ commented Route::* calls', () => {
  327. const src = `<?php
  328. // Route::get('/fake', [FakeController::class, 'index']);
  329. # Route::get('/also-fake', 'FakeController@show');
  330. /* Route::post('/another-fake', [X::class, 'y']); */
  331. Route::get('/real', [RealController::class, 'index']);
  332. `;
  333. const { nodes, references } = laravelResolver.extract!('routes/web.php', src);
  334. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  335. expect(references.map((r) => r.referenceName)).toEqual(['index']);
  336. });
  337. it('rails: skips =begin/=end and # commented routes', () => {
  338. const src = `
  339. # get '/fake', to: 'fake#index'
  340. =begin
  341. get '/also-fake', to: 'fake#show'
  342. =end
  343. get '/real', to: 'real#index'
  344. `;
  345. const { nodes, references } = railsResolver.extract!('config/routes.rb', src);
  346. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  347. expect(references.map((r) => r.referenceName)).toEqual(['index']);
  348. });
  349. it('spring: skips // and /* */ commented @GetMapping', () => {
  350. const src = `
  351. // @GetMapping("/fake")
  352. // public List<X> fake() { return null; }
  353. /* @PostMapping("/also-fake")
  354. public void alsoFake() {} */
  355. @GetMapping("/real")
  356. public List<User> listUsers() { return users; }
  357. `;
  358. const { nodes, references } = springResolver.extract!('UserController.java', src);
  359. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  360. expect(references.map((r) => r.referenceName)).toEqual(['listUsers']);
  361. });
  362. it('go: skips // and /* */ commented router.METHOD calls', () => {
  363. const src = `
  364. // r.GET("/fake", fakeHandler)
  365. /* r.POST("/also-fake", anotherHandler) */
  366. r.GET("/real", listUsers)
  367. `;
  368. const { nodes, references } = goResolver.extract!('main.go', src);
  369. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  370. expect(references.map((r) => r.referenceName)).toEqual(['listUsers']);
  371. });
  372. it('rust: skips // and nested /* */ commented .route() calls', () => {
  373. const src = `
  374. // .route("/fake", get(fake_handler))
  375. /* outer /* inner .route("/inner-fake", get(x)) */ still .route("/outer-fake", get(y)) */
  376. let app = Router::new().route("/real", get(list_users));
  377. `;
  378. const { nodes, references } = rustResolver.extract!('main.rs', src);
  379. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  380. expect(references.map((r) => r.referenceName)).toEqual(['list_users']);
  381. });
  382. it('aspnet: skips // and /* */ commented [HttpGet] attributes', () => {
  383. const src = `
  384. // [HttpGet("/fake")]
  385. // public IActionResult Fake() { return Ok(); }
  386. /* [HttpPost("/also-fake")]
  387. public IActionResult AlsoFake() { return Ok(); } */
  388. [HttpGet("/real")]
  389. public IActionResult ListUsers() { return Ok(); }
  390. `;
  391. const { nodes, references } = aspnetResolver.extract!('UserController.cs', src);
  392. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  393. expect(references.map((r) => r.referenceName)).toEqual(['ListUsers']);
  394. });
  395. it('vapor: skips // and /* */ commented app.METHOD calls', () => {
  396. const src = `
  397. // app.get("fake", use: fakeHandler)
  398. /* app.post("also-fake", use: anotherHandler) */
  399. app.get("real", use: listUsers)
  400. `;
  401. const { nodes, references } = vaporResolver.extract!('routes.swift', src);
  402. expect(nodes.map((n) => n.name)).toEqual(['GET real']);
  403. expect(references.map((r) => r.referenceName)).toEqual(['listUsers']);
  404. });
  405. });