extraction.test.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933
  1. /**
  2. * Extraction Tests
  3. *
  4. * Tests for the tree-sitter extraction system.
  5. */
  6. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  7. import * as fs from 'fs';
  8. import * as path from 'path';
  9. import * as os from 'os';
  10. import { CodeGraph } from '../src';
  11. import { extractFromSource } from '../src/extraction';
  12. import { detectLanguage, isLanguageSupported, getSupportedLanguages } from '../src/extraction/grammars';
  13. // Create a temporary directory for each test
  14. function createTempDir(): string {
  15. return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-'));
  16. }
  17. // Clean up temporary directory
  18. function cleanupTempDir(dir: string): void {
  19. if (fs.existsSync(dir)) {
  20. fs.rmSync(dir, { recursive: true, force: true });
  21. }
  22. }
  23. describe('Language Detection', () => {
  24. it('should detect TypeScript files', () => {
  25. expect(detectLanguage('src/index.ts')).toBe('typescript');
  26. expect(detectLanguage('components/Button.tsx')).toBe('tsx');
  27. });
  28. it('should detect JavaScript files', () => {
  29. expect(detectLanguage('index.js')).toBe('javascript');
  30. expect(detectLanguage('App.jsx')).toBe('jsx');
  31. expect(detectLanguage('config.mjs')).toBe('javascript');
  32. });
  33. it('should detect Python files', () => {
  34. expect(detectLanguage('main.py')).toBe('python');
  35. });
  36. it('should detect Go files', () => {
  37. expect(detectLanguage('main.go')).toBe('go');
  38. });
  39. it('should detect Rust files', () => {
  40. expect(detectLanguage('lib.rs')).toBe('rust');
  41. });
  42. it('should detect Java files', () => {
  43. expect(detectLanguage('Main.java')).toBe('java');
  44. });
  45. it('should detect C files', () => {
  46. expect(detectLanguage('main.c')).toBe('c');
  47. expect(detectLanguage('utils.h')).toBe('c');
  48. });
  49. it('should detect C++ files', () => {
  50. expect(detectLanguage('main.cpp')).toBe('cpp');
  51. expect(detectLanguage('class.hpp')).toBe('cpp');
  52. });
  53. it('should detect C# files', () => {
  54. expect(detectLanguage('Program.cs')).toBe('csharp');
  55. });
  56. it('should detect PHP files', () => {
  57. expect(detectLanguage('index.php')).toBe('php');
  58. });
  59. it('should detect Ruby files', () => {
  60. expect(detectLanguage('app.rb')).toBe('ruby');
  61. });
  62. it('should detect Swift files', () => {
  63. expect(detectLanguage('ViewController.swift')).toBe('swift');
  64. });
  65. it('should detect Kotlin files', () => {
  66. expect(detectLanguage('MainActivity.kt')).toBe('kotlin');
  67. expect(detectLanguage('build.gradle.kts')).toBe('kotlin');
  68. });
  69. it('should return unknown for unsupported extensions', () => {
  70. expect(detectLanguage('styles.css')).toBe('unknown');
  71. expect(detectLanguage('data.json')).toBe('unknown');
  72. });
  73. });
  74. describe('Language Support', () => {
  75. it('should report supported languages', () => {
  76. expect(isLanguageSupported('typescript')).toBe(true);
  77. expect(isLanguageSupported('python')).toBe(true);
  78. expect(isLanguageSupported('go')).toBe(true);
  79. expect(isLanguageSupported('unknown')).toBe(false);
  80. });
  81. it('should list all supported languages', () => {
  82. const languages = getSupportedLanguages();
  83. expect(languages).toContain('typescript');
  84. expect(languages).toContain('javascript');
  85. expect(languages).toContain('python');
  86. expect(languages).toContain('go');
  87. expect(languages).toContain('rust');
  88. expect(languages).toContain('java');
  89. expect(languages).toContain('csharp');
  90. expect(languages).toContain('php');
  91. expect(languages).toContain('ruby');
  92. expect(languages).toContain('swift');
  93. expect(languages).toContain('kotlin');
  94. });
  95. });
  96. describe('TypeScript Extraction', () => {
  97. it('should extract function declarations', () => {
  98. const code = `
  99. export function processPayment(amount: number): Promise<Receipt> {
  100. return stripe.charge(amount);
  101. }
  102. `;
  103. const result = extractFromSource('payment.ts', code);
  104. expect(result.nodes).toHaveLength(1);
  105. expect(result.nodes[0]).toMatchObject({
  106. kind: 'function',
  107. name: 'processPayment',
  108. language: 'typescript',
  109. isExported: true,
  110. });
  111. expect(result.nodes[0]?.signature).toContain('amount: number');
  112. });
  113. it('should extract class declarations', () => {
  114. const code = `
  115. export class PaymentService {
  116. private stripe: StripeClient;
  117. constructor(apiKey: string) {
  118. this.stripe = new StripeClient(apiKey);
  119. }
  120. async charge(amount: number): Promise<Receipt> {
  121. return this.stripe.charge(amount);
  122. }
  123. }
  124. `;
  125. const result = extractFromSource('service.ts', code);
  126. const classNode = result.nodes.find((n) => n.kind === 'class');
  127. const methodNodes = result.nodes.filter((n) => n.kind === 'method');
  128. expect(classNode).toBeDefined();
  129. expect(classNode?.name).toBe('PaymentService');
  130. expect(classNode?.isExported).toBe(true);
  131. expect(methodNodes.length).toBeGreaterThanOrEqual(1);
  132. const chargeMethod = methodNodes.find((m) => m.name === 'charge');
  133. expect(chargeMethod).toBeDefined();
  134. });
  135. it('should extract interfaces', () => {
  136. const code = `
  137. export interface User {
  138. id: string;
  139. name: string;
  140. email: string;
  141. }
  142. `;
  143. const result = extractFromSource('types.ts', code);
  144. expect(result.nodes).toHaveLength(1);
  145. expect(result.nodes[0]).toMatchObject({
  146. kind: 'interface',
  147. name: 'User',
  148. isExported: true,
  149. });
  150. });
  151. it('should track function calls', () => {
  152. const code = `
  153. function main() {
  154. const result = processData();
  155. console.log(result);
  156. }
  157. `;
  158. const result = extractFromSource('main.ts', code);
  159. expect(result.unresolvedReferences.length).toBeGreaterThan(0);
  160. const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
  161. expect(calls.some((c) => c.referenceName === 'processData')).toBe(true);
  162. });
  163. });
  164. describe('Arrow Function Export Extraction', () => {
  165. it('should extract exported arrow functions assigned to const', () => {
  166. const code = `
  167. export const useAuth = (): AuthContextValue => {
  168. return useContext(AuthContext);
  169. };
  170. `;
  171. const result = extractFromSource('hooks.ts', code);
  172. expect(result.nodes).toHaveLength(1);
  173. expect(result.nodes[0]).toMatchObject({
  174. kind: 'function',
  175. name: 'useAuth',
  176. isExported: true,
  177. });
  178. });
  179. it('should extract exported function expressions assigned to const', () => {
  180. const code = `
  181. export const processData = function(input: string): string {
  182. return input.trim();
  183. };
  184. `;
  185. const result = extractFromSource('utils.ts', code);
  186. expect(result.nodes).toHaveLength(1);
  187. expect(result.nodes[0]).toMatchObject({
  188. kind: 'function',
  189. name: 'processData',
  190. isExported: true,
  191. });
  192. });
  193. it('should not extract non-exported arrow functions as exported', () => {
  194. const code = `
  195. const internalHelper = () => {
  196. return 42;
  197. };
  198. `;
  199. const result = extractFromSource('internal.ts', code);
  200. const helperNode = result.nodes.find((n) => n.name === 'internalHelper');
  201. expect(helperNode).toBeDefined();
  202. expect(helperNode?.isExported).toBeFalsy();
  203. });
  204. it('should still skip truly anonymous arrow functions', () => {
  205. const code = `
  206. const items = [1, 2, 3].map((x) => x * 2);
  207. `;
  208. const result = extractFromSource('anon.ts', code);
  209. // The inline arrow function passed to .map() has no variable_declarator parent
  210. // and should remain anonymous (skipped)
  211. const anonFunctions = result.nodes.filter(
  212. (n) => n.kind === 'function' && n.name === '<anonymous>'
  213. );
  214. expect(anonFunctions).toHaveLength(0);
  215. });
  216. it('should extract multiple exported arrow functions from the same file', () => {
  217. const code = `
  218. export const add = (a: number, b: number): number => a + b;
  219. export const subtract = (a: number, b: number): number => a - b;
  220. const internal = () => 'not exported';
  221. `;
  222. const result = extractFromSource('math.ts', code);
  223. const exported = result.nodes.filter((n) => n.kind === 'function' && n.isExported);
  224. expect(exported).toHaveLength(2);
  225. expect(exported.map((n) => n.name).sort()).toEqual(['add', 'subtract']);
  226. const internalNode = result.nodes.find((n) => n.name === 'internal');
  227. expect(internalNode).toBeDefined();
  228. expect(internalNode?.isExported).toBeFalsy();
  229. });
  230. it('should extract arrow functions in JavaScript files', () => {
  231. const code = `
  232. export const fetchData = async () => {
  233. const response = await fetch('/api/data');
  234. return response.json();
  235. };
  236. `;
  237. const result = extractFromSource('api.js', code);
  238. expect(result.nodes).toHaveLength(1);
  239. expect(result.nodes[0]).toMatchObject({
  240. kind: 'function',
  241. name: 'fetchData',
  242. isExported: true,
  243. });
  244. });
  245. });
  246. describe('Type Alias Extraction', () => {
  247. it('should extract exported type aliases in TypeScript', () => {
  248. const code = `
  249. export type AuthContextValue = {
  250. user: User | null;
  251. login: () => void;
  252. logout: () => void;
  253. };
  254. `;
  255. const result = extractFromSource('types.ts', code);
  256. expect(result.nodes).toHaveLength(1);
  257. expect(result.nodes[0]).toMatchObject({
  258. kind: 'type_alias',
  259. name: 'AuthContextValue',
  260. isExported: true,
  261. });
  262. });
  263. it('should extract non-exported type aliases', () => {
  264. const code = `
  265. type InternalState = {
  266. loading: boolean;
  267. error: string | null;
  268. };
  269. `;
  270. const result = extractFromSource('internal.ts', code);
  271. expect(result.nodes).toHaveLength(1);
  272. expect(result.nodes[0]).toMatchObject({
  273. kind: 'type_alias',
  274. name: 'InternalState',
  275. isExported: false,
  276. });
  277. });
  278. it('should extract multiple type aliases from the same file', () => {
  279. const code = `
  280. export type UnitSystem = 'metric' | 'imperial';
  281. export type DateFormat = 'ISO' | 'US' | 'EU';
  282. type Internal = string;
  283. `;
  284. const result = extractFromSource('config.ts', code);
  285. const typeAliases = result.nodes.filter((n) => n.kind === 'type_alias');
  286. expect(typeAliases).toHaveLength(3);
  287. const exported = typeAliases.filter((n) => n.isExported);
  288. expect(exported).toHaveLength(2);
  289. expect(exported.map((n) => n.name).sort()).toEqual(['DateFormat', 'UnitSystem']);
  290. });
  291. });
  292. describe('Exported Variable Extraction', () => {
  293. it('should extract exported const with call expression (Zustand store)', () => {
  294. const code = `
  295. export const useUIStore = create<UIState>((set) => ({
  296. isOpen: false,
  297. toggle: () => set((s) => ({ isOpen: !s.isOpen })),
  298. }));
  299. `;
  300. const result = extractFromSource('store.ts', code);
  301. const varNode = result.nodes.find((n) => n.kind === 'variable' && n.name === 'useUIStore');
  302. expect(varNode).toBeDefined();
  303. expect(varNode?.isExported).toBe(true);
  304. });
  305. it('should extract exported const with object literal', () => {
  306. const code = `
  307. export const config = {
  308. apiUrl: 'https://api.example.com',
  309. timeout: 5000,
  310. };
  311. `;
  312. const result = extractFromSource('config.ts', code);
  313. const varNode = result.nodes.find((n) => n.kind === 'variable' && n.name === 'config');
  314. expect(varNode).toBeDefined();
  315. expect(varNode?.isExported).toBe(true);
  316. });
  317. it('should extract exported const with array literal', () => {
  318. const code = `
  319. export const SCREEN_NAMES = ['home', 'settings', 'profile'] as const;
  320. `;
  321. const result = extractFromSource('constants.ts', code);
  322. const varNode = result.nodes.find((n) => n.kind === 'variable' && n.name === 'SCREEN_NAMES');
  323. expect(varNode).toBeDefined();
  324. expect(varNode?.isExported).toBe(true);
  325. });
  326. it('should extract exported const with primitive value', () => {
  327. const code = `
  328. export const MAX_RETRIES = 3;
  329. export const API_VERSION = "v2";
  330. `;
  331. const result = extractFromSource('constants.ts', code);
  332. const variables = result.nodes.filter((n) => n.kind === 'variable');
  333. expect(variables).toHaveLength(2);
  334. expect(variables.map((n) => n.name).sort()).toEqual(['API_VERSION', 'MAX_RETRIES']);
  335. });
  336. it('should NOT duplicate arrow functions as both function and variable', () => {
  337. const code = `
  338. export const useAuth = () => {
  339. return useContext(AuthContext);
  340. };
  341. `;
  342. const result = extractFromSource('hooks.ts', code);
  343. // Should be extracted as function (from arrow function handler), NOT as variable
  344. const funcNodes = result.nodes.filter((n) => n.kind === 'function' && n.name === 'useAuth');
  345. const varNodes = result.nodes.filter((n) => n.kind === 'variable' && n.name === 'useAuth');
  346. expect(funcNodes).toHaveLength(1);
  347. expect(varNodes).toHaveLength(0);
  348. });
  349. it('should not extract non-exported const as exported variable', () => {
  350. const code = `
  351. const internalConfig = {
  352. debug: true,
  353. };
  354. `;
  355. const result = extractFromSource('internal.ts', code);
  356. // Non-exported const should NOT create a variable node
  357. // (only export_statement triggers extractExportedVariables)
  358. const varNodes = result.nodes.filter((n) => n.kind === 'variable' && n.name === 'internalConfig');
  359. expect(varNodes).toHaveLength(0);
  360. });
  361. it('should extract Zod schema exports', () => {
  362. const code = `
  363. export const userSchema = z.object({
  364. id: z.string(),
  365. name: z.string(),
  366. email: z.string().email(),
  367. });
  368. `;
  369. const result = extractFromSource('schemas.ts', code);
  370. const varNode = result.nodes.find((n) => n.kind === 'variable' && n.name === 'userSchema');
  371. expect(varNode).toBeDefined();
  372. expect(varNode?.isExported).toBe(true);
  373. });
  374. it('should extract XState machine exports', () => {
  375. const code = `
  376. export const authMachine = createMachine({
  377. id: "auth",
  378. initial: "idle",
  379. states: {
  380. idle: {},
  381. authenticated: {},
  382. },
  383. });
  384. `;
  385. const result = extractFromSource('machine.ts', code);
  386. const varNode = result.nodes.find((n) => n.kind === 'variable' && n.name === 'authMachine');
  387. expect(varNode).toBeDefined();
  388. expect(varNode?.isExported).toBe(true);
  389. });
  390. });
  391. describe('Python Extraction', () => {
  392. it('should extract function definitions', () => {
  393. const code = `
  394. def calculate_total(items: list, tax_rate: float) -> float:
  395. """Calculate total with tax."""
  396. subtotal = sum(item.price for item in items)
  397. return subtotal * (1 + tax_rate)
  398. `;
  399. const result = extractFromSource('calc.py', code);
  400. expect(result.nodes).toHaveLength(1);
  401. expect(result.nodes[0]).toMatchObject({
  402. kind: 'function',
  403. name: 'calculate_total',
  404. language: 'python',
  405. });
  406. });
  407. it('should extract class definitions', () => {
  408. const code = `
  409. class UserService:
  410. """Service for managing users."""
  411. def __init__(self, db):
  412. self.db = db
  413. def get_user(self, user_id: str) -> User:
  414. return self.db.find_user(user_id)
  415. `;
  416. const result = extractFromSource('service.py', code);
  417. const classNode = result.nodes.find((n) => n.kind === 'class');
  418. expect(classNode).toBeDefined();
  419. expect(classNode?.name).toBe('UserService');
  420. });
  421. });
  422. describe('Go Extraction', () => {
  423. it('should extract function declarations', () => {
  424. const code = `
  425. package main
  426. func ProcessOrder(order Order) (Receipt, error) {
  427. // Process the order
  428. return Receipt{}, nil
  429. }
  430. `;
  431. const result = extractFromSource('main.go', code);
  432. const funcNode = result.nodes.find((n) => n.kind === 'function');
  433. expect(funcNode).toBeDefined();
  434. expect(funcNode?.name).toBe('ProcessOrder');
  435. });
  436. it('should extract method declarations', () => {
  437. const code = `
  438. package main
  439. type Service struct {
  440. db *Database
  441. }
  442. func (s *Service) GetUser(id string) (*User, error) {
  443. return s.db.FindUser(id)
  444. }
  445. `;
  446. const result = extractFromSource('service.go', code);
  447. const methodNode = result.nodes.find((n) => n.kind === 'method');
  448. expect(methodNode).toBeDefined();
  449. expect(methodNode?.name).toBe('GetUser');
  450. });
  451. });
  452. describe('Rust Extraction', () => {
  453. it('should extract function declarations', () => {
  454. const code = `
  455. pub fn process_data(input: &str) -> Result<Output, Error> {
  456. // Process data
  457. Ok(Output::new())
  458. }
  459. `;
  460. const result = extractFromSource('lib.rs', code);
  461. const funcNode = result.nodes.find((n) => n.kind === 'function');
  462. expect(funcNode).toBeDefined();
  463. expect(funcNode?.name).toBe('process_data');
  464. expect(funcNode?.visibility).toBe('public');
  465. });
  466. it('should extract struct declarations', () => {
  467. const code = `
  468. pub struct User {
  469. pub id: String,
  470. pub name: String,
  471. email: String,
  472. }
  473. `;
  474. const result = extractFromSource('models.rs', code);
  475. const structNode = result.nodes.find((n) => n.kind === 'struct');
  476. expect(structNode).toBeDefined();
  477. expect(structNode?.name).toBe('User');
  478. });
  479. it('should extract trait declarations', () => {
  480. const code = `
  481. pub trait Repository {
  482. fn find(&self, id: &str) -> Option<Entity>;
  483. fn save(&mut self, entity: Entity) -> Result<(), Error>;
  484. }
  485. `;
  486. const result = extractFromSource('traits.rs', code);
  487. const traitNode = result.nodes.find((n) => n.kind === 'trait');
  488. expect(traitNode).toBeDefined();
  489. expect(traitNode?.name).toBe('Repository');
  490. });
  491. });
  492. describe('Java Extraction', () => {
  493. it('should extract class declarations', () => {
  494. const code = `
  495. public class UserService {
  496. private final UserRepository repository;
  497. public UserService(UserRepository repository) {
  498. this.repository = repository;
  499. }
  500. public User getUser(String id) {
  501. return repository.findById(id);
  502. }
  503. }
  504. `;
  505. const result = extractFromSource('UserService.java', code);
  506. const classNode = result.nodes.find((n) => n.kind === 'class');
  507. expect(classNode).toBeDefined();
  508. expect(classNode?.name).toBe('UserService');
  509. expect(classNode?.visibility).toBe('public');
  510. });
  511. it('should extract method declarations', () => {
  512. const code = `
  513. public class Calculator {
  514. public static int add(int a, int b) {
  515. return a + b;
  516. }
  517. }
  518. `;
  519. const result = extractFromSource('Calculator.java', code);
  520. const methodNode = result.nodes.find((n) => n.kind === 'method' && n.name === 'add');
  521. expect(methodNode).toBeDefined();
  522. expect(methodNode?.isStatic).toBe(true);
  523. });
  524. });
  525. describe('C# Extraction', () => {
  526. it('should extract class declarations', () => {
  527. const code = `
  528. public class OrderService
  529. {
  530. private readonly IOrderRepository _repository;
  531. public OrderService(IOrderRepository repository)
  532. {
  533. _repository = repository;
  534. }
  535. public async Task<Order> GetOrderAsync(string id)
  536. {
  537. return await _repository.FindByIdAsync(id);
  538. }
  539. }
  540. `;
  541. const result = extractFromSource('OrderService.cs', code);
  542. const classNode = result.nodes.find((n) => n.kind === 'class');
  543. expect(classNode).toBeDefined();
  544. expect(classNode?.name).toBe('OrderService');
  545. expect(classNode?.visibility).toBe('public');
  546. });
  547. });
  548. describe('PHP Extraction', () => {
  549. it('should extract class declarations', () => {
  550. const code = `<?php
  551. class UserController
  552. {
  553. private UserService $userService;
  554. public function __construct(UserService $userService)
  555. {
  556. $this->userService = $userService;
  557. }
  558. public function show(string $id): User
  559. {
  560. return $this->userService->find($id);
  561. }
  562. }
  563. `;
  564. const result = extractFromSource('UserController.php', code);
  565. const classNode = result.nodes.find((n) => n.kind === 'class');
  566. expect(classNode).toBeDefined();
  567. expect(classNode?.name).toBe('UserController');
  568. });
  569. });
  570. describe('Swift Extraction', () => {
  571. it('should extract class declarations', () => {
  572. const code = `
  573. public class NetworkManager {
  574. private let session: URLSession
  575. public init(session: URLSession = .shared) {
  576. self.session = session
  577. }
  578. public func fetchData(from url: URL) async throws -> Data {
  579. let (data, _) = try await session.data(from: url)
  580. return data
  581. }
  582. }
  583. `;
  584. const result = extractFromSource('NetworkManager.swift', code);
  585. const classNode = result.nodes.find((n) => n.kind === 'class');
  586. expect(classNode).toBeDefined();
  587. expect(classNode?.name).toBe('NetworkManager');
  588. });
  589. it('should extract function declarations', () => {
  590. const code = `
  591. func calculateSum(_ numbers: [Int]) -> Int {
  592. return numbers.reduce(0, +)
  593. }
  594. public func formatCurrency(amount: Double) -> String {
  595. return String(format: "$%.2f", amount)
  596. }
  597. `;
  598. const result = extractFromSource('utils.swift', code);
  599. const functions = result.nodes.filter((n) => n.kind === 'function');
  600. expect(functions.length).toBeGreaterThanOrEqual(1);
  601. });
  602. it('should extract struct declarations', () => {
  603. const code = `
  604. public struct User {
  605. let id: UUID
  606. var name: String
  607. var email: String
  608. func displayName() -> String {
  609. return name
  610. }
  611. }
  612. `;
  613. const result = extractFromSource('User.swift', code);
  614. const structNode = result.nodes.find((n) => n.kind === 'struct');
  615. expect(structNode).toBeDefined();
  616. expect(structNode?.name).toBe('User');
  617. });
  618. it('should extract protocol declarations', () => {
  619. const code = `
  620. public protocol Repository {
  621. associatedtype Entity
  622. func find(id: String) async throws -> Entity?
  623. func save(_ entity: Entity) async throws
  624. }
  625. `;
  626. const result = extractFromSource('Repository.swift', code);
  627. const protocolNode = result.nodes.find((n) => n.kind === 'interface');
  628. expect(protocolNode).toBeDefined();
  629. expect(protocolNode?.name).toBe('Repository');
  630. });
  631. });
  632. describe('Kotlin Extraction', () => {
  633. it('should extract class declarations', () => {
  634. const code = `
  635. class UserRepository(private val database: Database) {
  636. fun findById(id: String): User? {
  637. return database.query("SELECT * FROM users WHERE id = ?", id)
  638. }
  639. suspend fun save(user: User) {
  640. database.insert(user)
  641. }
  642. }
  643. `;
  644. const result = extractFromSource('UserRepository.kt', code);
  645. const classNode = result.nodes.find((n) => n.kind === 'class');
  646. expect(classNode).toBeDefined();
  647. expect(classNode?.name).toBe('UserRepository');
  648. });
  649. it('should extract function declarations', () => {
  650. const code = `
  651. fun calculateTotal(items: List<Item>): Double {
  652. return items.sumOf { it.price }
  653. }
  654. suspend fun fetchUserData(userId: String): User {
  655. return api.getUser(userId)
  656. }
  657. `;
  658. const result = extractFromSource('utils.kt', code);
  659. const functions = result.nodes.filter((n) => n.kind === 'function');
  660. expect(functions.length).toBeGreaterThanOrEqual(1);
  661. });
  662. it('should detect suspend functions as async', () => {
  663. const code = `
  664. suspend fun loadData(): List<String> {
  665. delay(1000)
  666. return listOf("a", "b", "c")
  667. }
  668. `;
  669. const result = extractFromSource('loader.kt', code);
  670. const funcNode = result.nodes.find((n) => n.kind === 'function');
  671. expect(funcNode).toBeDefined();
  672. expect(funcNode?.isAsync).toBe(true);
  673. });
  674. });
  675. describe('Full Indexing', () => {
  676. let tempDir: string;
  677. beforeEach(() => {
  678. tempDir = createTempDir();
  679. });
  680. afterEach(() => {
  681. cleanupTempDir(tempDir);
  682. });
  683. it('should index a TypeScript file', async () => {
  684. // Create test file
  685. const srcDir = path.join(tempDir, 'src');
  686. fs.mkdirSync(srcDir);
  687. fs.writeFileSync(
  688. path.join(srcDir, 'utils.ts'),
  689. `
  690. export function add(a: number, b: number): number {
  691. return a + b;
  692. }
  693. export function multiply(a: number, b: number): number {
  694. return a * b;
  695. }
  696. `
  697. );
  698. // Initialize and index
  699. const cg = CodeGraph.initSync(tempDir);
  700. const result = await cg.indexAll();
  701. expect(result.success).toBe(true);
  702. expect(result.filesIndexed).toBe(1);
  703. expect(result.nodesCreated).toBeGreaterThanOrEqual(2);
  704. // Check nodes were stored
  705. const nodes = cg.getNodesInFile('src/utils.ts');
  706. expect(nodes.length).toBeGreaterThanOrEqual(2);
  707. const addFunc = nodes.find((n) => n.name === 'add');
  708. expect(addFunc).toBeDefined();
  709. expect(addFunc?.kind).toBe('function');
  710. cg.close();
  711. });
  712. it('should index multiple files', async () => {
  713. // Create test files
  714. const srcDir = path.join(tempDir, 'src');
  715. fs.mkdirSync(srcDir);
  716. fs.writeFileSync(
  717. path.join(srcDir, 'math.ts'),
  718. `export function add(a: number, b: number) { return a + b; }`
  719. );
  720. fs.writeFileSync(
  721. path.join(srcDir, 'string.ts'),
  722. `export function capitalize(s: string) { return s.toUpperCase(); }`
  723. );
  724. // Initialize and index
  725. const cg = CodeGraph.initSync(tempDir);
  726. const result = await cg.indexAll();
  727. expect(result.success).toBe(true);
  728. expect(result.filesIndexed).toBe(2);
  729. const files = cg.getFiles();
  730. expect(files.length).toBe(2);
  731. cg.close();
  732. });
  733. it('should track file hashes for incremental updates', async () => {
  734. // Create initial file
  735. const srcDir = path.join(tempDir, 'src');
  736. fs.mkdirSync(srcDir);
  737. fs.writeFileSync(path.join(srcDir, 'main.ts'), `export const x = 1;`);
  738. // Initialize and index
  739. const cg = CodeGraph.initSync(tempDir);
  740. await cg.indexAll();
  741. // Check file is tracked
  742. const file = cg.getFile('src/main.ts');
  743. expect(file).toBeDefined();
  744. expect(file?.contentHash).toBeDefined();
  745. // Modify file
  746. fs.writeFileSync(path.join(srcDir, 'main.ts'), `export const x = 2;`);
  747. // Check for changes
  748. const changes = cg.getChangedFiles();
  749. expect(changes.modified).toContain('src/main.ts');
  750. cg.close();
  751. });
  752. it('should sync and detect changes', async () => {
  753. // Create initial file
  754. const srcDir = path.join(tempDir, 'src');
  755. fs.mkdirSync(srcDir);
  756. fs.writeFileSync(
  757. path.join(srcDir, 'main.ts'),
  758. `export function original() { return 1; }`
  759. );
  760. // Initialize and index
  761. const cg = CodeGraph.initSync(tempDir);
  762. await cg.indexAll();
  763. const initialNodes = cg.getNodesInFile('src/main.ts');
  764. expect(initialNodes.some((n) => n.name === 'original')).toBe(true);
  765. // Modify file
  766. fs.writeFileSync(
  767. path.join(srcDir, 'main.ts'),
  768. `export function updated() { return 2; }`
  769. );
  770. // Sync
  771. const syncResult = await cg.sync();
  772. expect(syncResult.filesModified).toBe(1);
  773. // Check nodes were updated
  774. const updatedNodes = cg.getNodesInFile('src/main.ts');
  775. expect(updatedNodes.some((n) => n.name === 'updated')).toBe(true);
  776. expect(updatedNodes.some((n) => n.name === 'original')).toBe(false);
  777. cg.close();
  778. });
  779. });