frameworks.test.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  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. describe('rustResolver.resolve cargo workspace crates', () => {
  227. it('resolves crate name from workspace member lib.rs', () => {
  228. const workspaceCargo = `
  229. [workspace]
  230. members = ["crates/mytool-core", "crates/mytool-fetcher"]
  231. `;
  232. const coreCargo = `
  233. [package]
  234. name = "mytool-core"
  235. version = "0.1.0"
  236. `;
  237. const libNode: Node = {
  238. id: 'module:crates/mytool-core/src/lib.rs:mytool_core:1',
  239. kind: 'module',
  240. name: 'mytool_core',
  241. qualifiedName: 'crates/mytool-core/src/lib.rs::mytool_core',
  242. filePath: 'crates/mytool-core/src/lib.rs',
  243. language: 'rust',
  244. startLine: 1,
  245. endLine: 1,
  246. startColumn: 0,
  247. endColumn: 0,
  248. updatedAt: Date.now(),
  249. };
  250. const context = {
  251. getNodesInFile: (fp: string) => (fp === 'crates/mytool-core/src/lib.rs' ? [libNode] : []),
  252. getNodesByName: () => [],
  253. getNodesByQualifiedName: () => [],
  254. getNodesByKind: () => [],
  255. fileExists: (p: string) => (
  256. p === 'Cargo.toml' ||
  257. p === 'crates/mytool-core/Cargo.toml' ||
  258. p === 'crates/mytool-core/src/lib.rs'
  259. ),
  260. readFile: (p: string) => {
  261. if (p === 'Cargo.toml') return workspaceCargo;
  262. if (p === 'crates/mytool-core/Cargo.toml') return coreCargo;
  263. return null;
  264. },
  265. getProjectRoot: () => '/test',
  266. getAllFiles: () => [
  267. 'Cargo.toml',
  268. 'crates/mytool-core/Cargo.toml',
  269. 'crates/mytool-core/src/lib.rs',
  270. ],
  271. getNodesByLowerName: () => [],
  272. getImportMappings: () => [],
  273. };
  274. const ref = {
  275. fromNodeId: 'fn:crates/mytool-fetcher/src/main.rs:main:1',
  276. referenceName: 'mytool_core',
  277. referenceKind: 'references' as const,
  278. line: 1,
  279. column: 1,
  280. filePath: 'crates/mytool-fetcher/src/main.rs',
  281. language: 'rust' as const,
  282. };
  283. const result = rustResolver.resolve(ref, context);
  284. expect(result?.targetNodeId).toBe(libNode.id);
  285. expect(result?.resolvedBy).toBe('framework');
  286. // Workspace-manifest hits are unambiguous and must beat name-matcher's
  287. // self-file matches (0.7) so cross-crate `imports` edges materialize.
  288. expect(result?.confidence).toBeGreaterThanOrEqual(0.9);
  289. });
  290. it('resolves crate name from workspace member main.rs when lib.rs is absent', () => {
  291. const workspaceCargo = `
  292. [workspace]
  293. members = [
  294. "crates/mytool-runner",
  295. ]
  296. `;
  297. const runnerCargo = `
  298. [package]
  299. name = "mytool-runner"
  300. version = "0.1.0"
  301. `;
  302. const mainNode: Node = {
  303. id: 'module:crates/mytool-runner/src/main.rs:mytool_runner:1',
  304. kind: 'module',
  305. name: 'mytool_runner',
  306. qualifiedName: 'crates/mytool-runner/src/main.rs::mytool_runner',
  307. filePath: 'crates/mytool-runner/src/main.rs',
  308. language: 'rust',
  309. startLine: 1,
  310. endLine: 1,
  311. startColumn: 0,
  312. endColumn: 0,
  313. updatedAt: Date.now(),
  314. };
  315. const context = {
  316. getNodesInFile: (fp: string) => (fp === 'crates/mytool-runner/src/main.rs' ? [mainNode] : []),
  317. getNodesByName: () => [],
  318. getNodesByQualifiedName: () => [],
  319. getNodesByKind: () => [],
  320. fileExists: (p: string) => (
  321. p === 'Cargo.toml' ||
  322. p === 'crates/mytool-runner/Cargo.toml' ||
  323. p === 'crates/mytool-runner/src/main.rs'
  324. ),
  325. readFile: (p: string) => {
  326. if (p === 'Cargo.toml') return workspaceCargo;
  327. if (p === 'crates/mytool-runner/Cargo.toml') return runnerCargo;
  328. return null;
  329. },
  330. getProjectRoot: () => '/test',
  331. getAllFiles: () => [
  332. 'Cargo.toml',
  333. 'crates/mytool-runner/Cargo.toml',
  334. 'crates/mytool-runner/src/main.rs',
  335. ],
  336. getNodesByLowerName: () => [],
  337. getImportMappings: () => [],
  338. };
  339. const ref = {
  340. fromNodeId: 'fn:crates/mytool-runner/src/main.rs:main:1',
  341. referenceName: 'mytool_runner',
  342. referenceKind: 'references' as const,
  343. line: 1,
  344. column: 1,
  345. filePath: 'crates/mytool-runner/src/main.rs',
  346. language: 'rust' as const,
  347. };
  348. const result = rustResolver.resolve(ref, context);
  349. expect(result?.targetNodeId).toBe(mainNode.id);
  350. expect(result?.resolvedBy).toBe('framework');
  351. });
  352. it('resolves crate name when members uses a glob (crates/*)', () => {
  353. const workspaceCargo = `
  354. [workspace]
  355. members = ["crates/*"]
  356. `;
  357. const fooCargo = `
  358. [package]
  359. name = "mytool-foo"
  360. version = "0.1.0"
  361. `;
  362. const barCargo = `
  363. [package]
  364. name = "mytool-bar"
  365. version = "0.1.0"
  366. `;
  367. const fooLib: Node = {
  368. id: 'module:crates/mytool-foo/src/lib.rs:mytool_foo:1',
  369. kind: 'module',
  370. name: 'mytool_foo',
  371. qualifiedName: 'crates/mytool-foo/src/lib.rs::mytool_foo',
  372. filePath: 'crates/mytool-foo/src/lib.rs',
  373. language: 'rust',
  374. startLine: 1,
  375. endLine: 1,
  376. startColumn: 0,
  377. endColumn: 0,
  378. updatedAt: Date.now(),
  379. };
  380. const barLib: Node = {
  381. id: 'module:crates/mytool-bar/src/lib.rs:mytool_bar:1',
  382. kind: 'module',
  383. name: 'mytool_bar',
  384. qualifiedName: 'crates/mytool-bar/src/lib.rs::mytool_bar',
  385. filePath: 'crates/mytool-bar/src/lib.rs',
  386. language: 'rust',
  387. startLine: 1,
  388. endLine: 1,
  389. startColumn: 0,
  390. endColumn: 0,
  391. updatedAt: Date.now(),
  392. };
  393. const filesByPath: Record<string, string> = {
  394. 'Cargo.toml': workspaceCargo,
  395. 'crates/mytool-foo/Cargo.toml': fooCargo,
  396. 'crates/mytool-bar/Cargo.toml': barCargo,
  397. };
  398. const nodesByFile: Record<string, Node[]> = {
  399. 'crates/mytool-foo/src/lib.rs': [fooLib],
  400. 'crates/mytool-bar/src/lib.rs': [barLib],
  401. };
  402. const dirsByPath: Record<string, string[]> = {
  403. '.': ['crates'],
  404. crates: ['mytool-foo', 'mytool-bar'],
  405. 'crates/mytool-foo': ['src'],
  406. 'crates/mytool-bar': ['src'],
  407. };
  408. const context = {
  409. getNodesInFile: (fp: string) => nodesByFile[fp] ?? [],
  410. getNodesByName: () => [],
  411. getNodesByQualifiedName: () => [],
  412. getNodesByKind: () => [],
  413. fileExists: (p: string) => (
  414. Object.prototype.hasOwnProperty.call(filesByPath, p) ||
  415. Object.prototype.hasOwnProperty.call(nodesByFile, p)
  416. ),
  417. readFile: (p: string) => filesByPath[p] ?? null,
  418. getProjectRoot: () => '/test',
  419. getAllFiles: () => [
  420. 'Cargo.toml',
  421. ...Object.keys(filesByPath).filter((p) => p !== 'Cargo.toml'),
  422. ...Object.keys(nodesByFile),
  423. ],
  424. getNodesByLowerName: () => [],
  425. getImportMappings: () => [],
  426. listDirectories: (rel: string) => dirsByPath[rel] ?? [],
  427. };
  428. const fooRef = {
  429. fromNodeId: 'fn:crates/mytool-bar/src/lib.rs:other:1',
  430. referenceName: 'mytool_foo',
  431. referenceKind: 'references' as const,
  432. line: 1,
  433. column: 1,
  434. filePath: 'crates/mytool-bar/src/lib.rs',
  435. language: 'rust' as const,
  436. };
  437. const barRef = {
  438. fromNodeId: 'fn:crates/mytool-foo/src/lib.rs:other:1',
  439. referenceName: 'mytool_bar',
  440. referenceKind: 'references' as const,
  441. line: 1,
  442. column: 1,
  443. filePath: 'crates/mytool-foo/src/lib.rs',
  444. language: 'rust' as const,
  445. };
  446. expect(rustResolver.resolve(fooRef, context)?.targetNodeId).toBe(fooLib.id);
  447. expect(rustResolver.resolve(barRef, context)?.targetNodeId).toBe(barLib.id);
  448. });
  449. it('resolves crate name when members uses a name glob at root (helix-*)', () => {
  450. const workspaceCargo = `
  451. [workspace]
  452. members = ["helix-*"]
  453. `;
  454. const coreCargo = `
  455. [package]
  456. name = "helix-core"
  457. version = "0.1.0"
  458. `;
  459. const coreLib: Node = {
  460. id: 'module:helix-core/src/lib.rs:helix_core:1',
  461. kind: 'module',
  462. name: 'helix_core',
  463. qualifiedName: 'helix-core/src/lib.rs::helix_core',
  464. filePath: 'helix-core/src/lib.rs',
  465. language: 'rust',
  466. startLine: 1,
  467. endLine: 1,
  468. startColumn: 0,
  469. endColumn: 0,
  470. updatedAt: Date.now(),
  471. };
  472. const filesByPath: Record<string, string> = {
  473. 'Cargo.toml': workspaceCargo,
  474. 'helix-core/Cargo.toml': coreCargo,
  475. };
  476. const nodesByFile: Record<string, Node[]> = {
  477. 'helix-core/src/lib.rs': [coreLib],
  478. };
  479. const dirsByPath: Record<string, string[]> = {
  480. '.': ['helix-core', 'docs', 'target'],
  481. 'helix-core': ['src'],
  482. };
  483. const context = {
  484. getNodesInFile: (fp: string) => nodesByFile[fp] ?? [],
  485. getNodesByName: () => [],
  486. getNodesByQualifiedName: () => [],
  487. getNodesByKind: () => [],
  488. fileExists: (p: string) => (
  489. Object.prototype.hasOwnProperty.call(filesByPath, p) ||
  490. Object.prototype.hasOwnProperty.call(nodesByFile, p)
  491. ),
  492. readFile: (p: string) => filesByPath[p] ?? null,
  493. getProjectRoot: () => '/test',
  494. getAllFiles: () => [
  495. 'Cargo.toml',
  496. ...Object.keys(filesByPath).filter((p) => p !== 'Cargo.toml'),
  497. ...Object.keys(nodesByFile),
  498. ],
  499. getNodesByLowerName: () => [],
  500. getImportMappings: () => [],
  501. listDirectories: (rel: string) => dirsByPath[rel] ?? [],
  502. };
  503. const ref = {
  504. fromNodeId: 'fn:helix-core/src/lib.rs:other:1',
  505. referenceName: 'helix_core',
  506. referenceKind: 'references' as const,
  507. line: 1,
  508. column: 1,
  509. filePath: 'helix-core/src/lib.rs',
  510. language: 'rust' as const,
  511. };
  512. expect(rustResolver.resolve(ref, context)?.targetNodeId).toBe(coreLib.id);
  513. });
  514. });
  515. import { aspnetResolver } from '../src/resolution/frameworks/csharp';
  516. describe('aspnetResolver.extract', () => {
  517. it('extracts route from [HttpGet] attribute', () => {
  518. const src = `
  519. [HttpGet("/users")]
  520. public IActionResult ListUsers()
  521. {
  522. return Ok();
  523. }
  524. `;
  525. const { nodes, references } = aspnetResolver.extract!('UserController.cs', src);
  526. expect(nodes[0].name).toBe('GET /users');
  527. expect(references[0].referenceName).toBe('ListUsers');
  528. });
  529. });
  530. import { vaporResolver } from '../src/resolution/frameworks/swift';
  531. describe('vaporResolver.extract', () => {
  532. it('extracts route from app.get with use:', () => {
  533. const src = `app.get("users", use: listUsers)\n`;
  534. const { nodes, references } = vaporResolver.extract!('routes.swift', src);
  535. expect(nodes[0].name).toBe('GET users');
  536. expect(references[0].referenceName).toBe('listUsers');
  537. });
  538. });
  539. import { reactResolver } from '../src/resolution/frameworks/react';
  540. import { svelteResolver } from '../src/resolution/frameworks/svelte';
  541. describe('reactResolver.extract (smoke)', () => {
  542. it('returns { nodes, references } shape', () => {
  543. const src = `<Route path="/users" element={<UsersPage/>}/>`;
  544. const result = reactResolver.extract!('App.tsx', src);
  545. expect(result).toHaveProperty('nodes');
  546. expect(result).toHaveProperty('references');
  547. expect(Array.isArray(result.nodes)).toBe(true);
  548. expect(Array.isArray(result.references)).toBe(true);
  549. });
  550. });
  551. describe('svelteResolver.extract (smoke)', () => {
  552. it('returns { nodes, references } shape', () => {
  553. const result = svelteResolver.extract!('+page.svelte', '');
  554. expect(result).toHaveProperty('nodes');
  555. expect(result).toHaveProperty('references');
  556. });
  557. });
  558. // Regression tests: commented-out and docstring route examples must NOT
  559. // surface as phantom route nodes. These would have failed before the
  560. // strip-comments wiring (the regex would happily scan comments/docstrings).
  561. describe('framework extractors ignore commented-out routes', () => {
  562. it('django: skips line-comment and docstring routes', () => {
  563. const src = `
  564. # urls.py example:
  565. # path('/admin/', AdminPanel.as_view())
  566. """
  567. Other routing example:
  568. path('/users/', UserListView.as_view())
  569. """
  570. urlpatterns = [path('/real/', RealView.as_view())]
  571. `;
  572. const result = djangoResolver.extract!('app/urls.py', src);
  573. const urls = result.nodes.map((n) => n.name);
  574. expect(urls).toEqual(['/real/']);
  575. });
  576. it('flask: skips commented-out @app.route', () => {
  577. const src = `
  578. # @app.route('/fake')
  579. # def fake_view():
  580. # return ''
  581. @app.route('/real')
  582. def real_view():
  583. return ''
  584. `;
  585. const { nodes, references } = flaskResolver.extract!('app.py', src);
  586. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  587. expect(references.map((r) => r.referenceName)).toEqual(['real_view']);
  588. });
  589. it('fastapi: skips docstring example routes', () => {
  590. const src = `
  591. """
  592. Example:
  593. @app.get('/in-docstring')
  594. async def doc():
  595. pass
  596. """
  597. @app.get('/real')
  598. async def real_handler():
  599. return {}
  600. `;
  601. const { nodes, references } = fastapiResolver.extract!('main.py', src);
  602. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  603. expect(references.map((r) => r.referenceName)).toEqual(['real_handler']);
  604. });
  605. it('express: skips // and /* */ commented routes', () => {
  606. const src = `
  607. // app.get('/fake', fakeHandler);
  608. /* router.post('/also-fake', otherHandler); */
  609. app.get('/real', realHandler);
  610. `;
  611. const { nodes, references } = expressResolver.extract!('routes.ts', src);
  612. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  613. expect(references.map((r) => r.referenceName)).toEqual(['realHandler']);
  614. });
  615. it('laravel: skips // # and /* */ commented Route::* calls', () => {
  616. const src = `<?php
  617. // Route::get('/fake', [FakeController::class, 'index']);
  618. # Route::get('/also-fake', 'FakeController@show');
  619. /* Route::post('/another-fake', [X::class, 'y']); */
  620. Route::get('/real', [RealController::class, 'index']);
  621. `;
  622. const { nodes, references } = laravelResolver.extract!('routes/web.php', src);
  623. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  624. expect(references.map((r) => r.referenceName)).toEqual(['index']);
  625. });
  626. it('rails: skips =begin/=end and # commented routes', () => {
  627. const src = `
  628. # get '/fake', to: 'fake#index'
  629. =begin
  630. get '/also-fake', to: 'fake#show'
  631. =end
  632. get '/real', to: 'real#index'
  633. `;
  634. const { nodes, references } = railsResolver.extract!('config/routes.rb', src);
  635. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  636. expect(references.map((r) => r.referenceName)).toEqual(['index']);
  637. });
  638. it('spring: skips // and /* */ commented @GetMapping', () => {
  639. const src = `
  640. // @GetMapping("/fake")
  641. // public List<X> fake() { return null; }
  642. /* @PostMapping("/also-fake")
  643. public void alsoFake() {} */
  644. @GetMapping("/real")
  645. public List<User> listUsers() { return users; }
  646. `;
  647. const { nodes, references } = springResolver.extract!('UserController.java', src);
  648. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  649. expect(references.map((r) => r.referenceName)).toEqual(['listUsers']);
  650. });
  651. it('go: skips // and /* */ commented router.METHOD calls', () => {
  652. const src = `
  653. // r.GET("/fake", fakeHandler)
  654. /* r.POST("/also-fake", anotherHandler) */
  655. r.GET("/real", listUsers)
  656. `;
  657. const { nodes, references } = goResolver.extract!('main.go', src);
  658. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  659. expect(references.map((r) => r.referenceName)).toEqual(['listUsers']);
  660. });
  661. it('rust: skips // and nested /* */ commented .route() calls', () => {
  662. const src = `
  663. // .route("/fake", get(fake_handler))
  664. /* outer /* inner .route("/inner-fake", get(x)) */ still .route("/outer-fake", get(y)) */
  665. let app = Router::new().route("/real", get(list_users));
  666. `;
  667. const { nodes, references } = rustResolver.extract!('main.rs', src);
  668. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  669. expect(references.map((r) => r.referenceName)).toEqual(['list_users']);
  670. });
  671. it('aspnet: skips // and /* */ commented [HttpGet] attributes', () => {
  672. const src = `
  673. // [HttpGet("/fake")]
  674. // public IActionResult Fake() { return Ok(); }
  675. /* [HttpPost("/also-fake")]
  676. public IActionResult AlsoFake() { return Ok(); } */
  677. [HttpGet("/real")]
  678. public IActionResult ListUsers() { return Ok(); }
  679. `;
  680. const { nodes, references } = aspnetResolver.extract!('UserController.cs', src);
  681. expect(nodes.map((n) => n.name)).toEqual(['GET /real']);
  682. expect(references.map((r) => r.referenceName)).toEqual(['ListUsers']);
  683. });
  684. it('vapor: skips // and /* */ commented app.METHOD calls', () => {
  685. const src = `
  686. // app.get("fake", use: fakeHandler)
  687. /* app.post("also-fake", use: anotherHandler) */
  688. app.get("real", use: listUsers)
  689. `;
  690. const { nodes, references } = vaporResolver.extract!('routes.swift', src);
  691. expect(nodes.map((n) => n.name)).toEqual(['GET real']);
  692. expect(references.map((r) => r.referenceName)).toEqual(['listUsers']);
  693. });
  694. });