1
0

extraction.test.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  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('Python Extraction', () => {
  165. it('should extract function definitions', () => {
  166. const code = `
  167. def calculate_total(items: list, tax_rate: float) -> float:
  168. """Calculate total with tax."""
  169. subtotal = sum(item.price for item in items)
  170. return subtotal * (1 + tax_rate)
  171. `;
  172. const result = extractFromSource('calc.py', code);
  173. expect(result.nodes).toHaveLength(1);
  174. expect(result.nodes[0]).toMatchObject({
  175. kind: 'function',
  176. name: 'calculate_total',
  177. language: 'python',
  178. });
  179. });
  180. it('should extract class definitions', () => {
  181. const code = `
  182. class UserService:
  183. """Service for managing users."""
  184. def __init__(self, db):
  185. self.db = db
  186. def get_user(self, user_id: str) -> User:
  187. return self.db.find_user(user_id)
  188. `;
  189. const result = extractFromSource('service.py', code);
  190. const classNode = result.nodes.find((n) => n.kind === 'class');
  191. expect(classNode).toBeDefined();
  192. expect(classNode?.name).toBe('UserService');
  193. });
  194. });
  195. describe('Go Extraction', () => {
  196. it('should extract function declarations', () => {
  197. const code = `
  198. package main
  199. func ProcessOrder(order Order) (Receipt, error) {
  200. // Process the order
  201. return Receipt{}, nil
  202. }
  203. `;
  204. const result = extractFromSource('main.go', code);
  205. const funcNode = result.nodes.find((n) => n.kind === 'function');
  206. expect(funcNode).toBeDefined();
  207. expect(funcNode?.name).toBe('ProcessOrder');
  208. });
  209. it('should extract method declarations', () => {
  210. const code = `
  211. package main
  212. type Service struct {
  213. db *Database
  214. }
  215. func (s *Service) GetUser(id string) (*User, error) {
  216. return s.db.FindUser(id)
  217. }
  218. `;
  219. const result = extractFromSource('service.go', code);
  220. const methodNode = result.nodes.find((n) => n.kind === 'method');
  221. expect(methodNode).toBeDefined();
  222. expect(methodNode?.name).toBe('GetUser');
  223. });
  224. });
  225. describe('Rust Extraction', () => {
  226. it('should extract function declarations', () => {
  227. const code = `
  228. pub fn process_data(input: &str) -> Result<Output, Error> {
  229. // Process data
  230. Ok(Output::new())
  231. }
  232. `;
  233. const result = extractFromSource('lib.rs', code);
  234. const funcNode = result.nodes.find((n) => n.kind === 'function');
  235. expect(funcNode).toBeDefined();
  236. expect(funcNode?.name).toBe('process_data');
  237. expect(funcNode?.visibility).toBe('public');
  238. });
  239. it('should extract struct declarations', () => {
  240. const code = `
  241. pub struct User {
  242. pub id: String,
  243. pub name: String,
  244. email: String,
  245. }
  246. `;
  247. const result = extractFromSource('models.rs', code);
  248. const structNode = result.nodes.find((n) => n.kind === 'struct');
  249. expect(structNode).toBeDefined();
  250. expect(structNode?.name).toBe('User');
  251. });
  252. it('should extract trait declarations', () => {
  253. const code = `
  254. pub trait Repository {
  255. fn find(&self, id: &str) -> Option<Entity>;
  256. fn save(&mut self, entity: Entity) -> Result<(), Error>;
  257. }
  258. `;
  259. const result = extractFromSource('traits.rs', code);
  260. const traitNode = result.nodes.find((n) => n.kind === 'trait');
  261. expect(traitNode).toBeDefined();
  262. expect(traitNode?.name).toBe('Repository');
  263. });
  264. });
  265. describe('Java Extraction', () => {
  266. it('should extract class declarations', () => {
  267. const code = `
  268. public class UserService {
  269. private final UserRepository repository;
  270. public UserService(UserRepository repository) {
  271. this.repository = repository;
  272. }
  273. public User getUser(String id) {
  274. return repository.findById(id);
  275. }
  276. }
  277. `;
  278. const result = extractFromSource('UserService.java', code);
  279. const classNode = result.nodes.find((n) => n.kind === 'class');
  280. expect(classNode).toBeDefined();
  281. expect(classNode?.name).toBe('UserService');
  282. expect(classNode?.visibility).toBe('public');
  283. });
  284. it('should extract method declarations', () => {
  285. const code = `
  286. public class Calculator {
  287. public static int add(int a, int b) {
  288. return a + b;
  289. }
  290. }
  291. `;
  292. const result = extractFromSource('Calculator.java', code);
  293. const methodNode = result.nodes.find((n) => n.kind === 'method' && n.name === 'add');
  294. expect(methodNode).toBeDefined();
  295. expect(methodNode?.isStatic).toBe(true);
  296. });
  297. });
  298. describe('C# Extraction', () => {
  299. it('should extract class declarations', () => {
  300. const code = `
  301. public class OrderService
  302. {
  303. private readonly IOrderRepository _repository;
  304. public OrderService(IOrderRepository repository)
  305. {
  306. _repository = repository;
  307. }
  308. public async Task<Order> GetOrderAsync(string id)
  309. {
  310. return await _repository.FindByIdAsync(id);
  311. }
  312. }
  313. `;
  314. const result = extractFromSource('OrderService.cs', code);
  315. const classNode = result.nodes.find((n) => n.kind === 'class');
  316. expect(classNode).toBeDefined();
  317. expect(classNode?.name).toBe('OrderService');
  318. expect(classNode?.visibility).toBe('public');
  319. });
  320. });
  321. describe('PHP Extraction', () => {
  322. it('should extract class declarations', () => {
  323. const code = `<?php
  324. class UserController
  325. {
  326. private UserService $userService;
  327. public function __construct(UserService $userService)
  328. {
  329. $this->userService = $userService;
  330. }
  331. public function show(string $id): User
  332. {
  333. return $this->userService->find($id);
  334. }
  335. }
  336. `;
  337. const result = extractFromSource('UserController.php', code);
  338. const classNode = result.nodes.find((n) => n.kind === 'class');
  339. expect(classNode).toBeDefined();
  340. expect(classNode?.name).toBe('UserController');
  341. });
  342. });
  343. describe('Swift Extraction', () => {
  344. it('should extract class declarations', () => {
  345. const code = `
  346. public class NetworkManager {
  347. private let session: URLSession
  348. public init(session: URLSession = .shared) {
  349. self.session = session
  350. }
  351. public func fetchData(from url: URL) async throws -> Data {
  352. let (data, _) = try await session.data(from: url)
  353. return data
  354. }
  355. }
  356. `;
  357. const result = extractFromSource('NetworkManager.swift', code);
  358. const classNode = result.nodes.find((n) => n.kind === 'class');
  359. expect(classNode).toBeDefined();
  360. expect(classNode?.name).toBe('NetworkManager');
  361. });
  362. it('should extract function declarations', () => {
  363. const code = `
  364. func calculateSum(_ numbers: [Int]) -> Int {
  365. return numbers.reduce(0, +)
  366. }
  367. public func formatCurrency(amount: Double) -> String {
  368. return String(format: "$%.2f", amount)
  369. }
  370. `;
  371. const result = extractFromSource('utils.swift', code);
  372. const functions = result.nodes.filter((n) => n.kind === 'function');
  373. expect(functions.length).toBeGreaterThanOrEqual(1);
  374. });
  375. it('should extract struct declarations', () => {
  376. const code = `
  377. public struct User {
  378. let id: UUID
  379. var name: String
  380. var email: String
  381. func displayName() -> String {
  382. return name
  383. }
  384. }
  385. `;
  386. const result = extractFromSource('User.swift', code);
  387. const structNode = result.nodes.find((n) => n.kind === 'struct');
  388. expect(structNode).toBeDefined();
  389. expect(structNode?.name).toBe('User');
  390. });
  391. it('should extract protocol declarations', () => {
  392. const code = `
  393. public protocol Repository {
  394. associatedtype Entity
  395. func find(id: String) async throws -> Entity?
  396. func save(_ entity: Entity) async throws
  397. }
  398. `;
  399. const result = extractFromSource('Repository.swift', code);
  400. const protocolNode = result.nodes.find((n) => n.kind === 'interface');
  401. expect(protocolNode).toBeDefined();
  402. expect(protocolNode?.name).toBe('Repository');
  403. });
  404. });
  405. describe('Kotlin Extraction', () => {
  406. it('should extract class declarations', () => {
  407. const code = `
  408. class UserRepository(private val database: Database) {
  409. fun findById(id: String): User? {
  410. return database.query("SELECT * FROM users WHERE id = ?", id)
  411. }
  412. suspend fun save(user: User) {
  413. database.insert(user)
  414. }
  415. }
  416. `;
  417. const result = extractFromSource('UserRepository.kt', code);
  418. const classNode = result.nodes.find((n) => n.kind === 'class');
  419. expect(classNode).toBeDefined();
  420. expect(classNode?.name).toBe('UserRepository');
  421. });
  422. it('should extract function declarations', () => {
  423. const code = `
  424. fun calculateTotal(items: List<Item>): Double {
  425. return items.sumOf { it.price }
  426. }
  427. suspend fun fetchUserData(userId: String): User {
  428. return api.getUser(userId)
  429. }
  430. `;
  431. const result = extractFromSource('utils.kt', code);
  432. const functions = result.nodes.filter((n) => n.kind === 'function');
  433. expect(functions.length).toBeGreaterThanOrEqual(1);
  434. });
  435. it('should detect suspend functions as async', () => {
  436. const code = `
  437. suspend fun loadData(): List<String> {
  438. delay(1000)
  439. return listOf("a", "b", "c")
  440. }
  441. `;
  442. const result = extractFromSource('loader.kt', code);
  443. const funcNode = result.nodes.find((n) => n.kind === 'function');
  444. expect(funcNode).toBeDefined();
  445. expect(funcNode?.isAsync).toBe(true);
  446. });
  447. });
  448. describe('Full Indexing', () => {
  449. let tempDir: string;
  450. beforeEach(() => {
  451. tempDir = createTempDir();
  452. });
  453. afterEach(() => {
  454. cleanupTempDir(tempDir);
  455. });
  456. it('should index a TypeScript file', async () => {
  457. // Create test file
  458. const srcDir = path.join(tempDir, 'src');
  459. fs.mkdirSync(srcDir);
  460. fs.writeFileSync(
  461. path.join(srcDir, 'utils.ts'),
  462. `
  463. export function add(a: number, b: number): number {
  464. return a + b;
  465. }
  466. export function multiply(a: number, b: number): number {
  467. return a * b;
  468. }
  469. `
  470. );
  471. // Initialize and index
  472. const cg = CodeGraph.initSync(tempDir);
  473. const result = await cg.indexAll();
  474. expect(result.success).toBe(true);
  475. expect(result.filesIndexed).toBe(1);
  476. expect(result.nodesCreated).toBeGreaterThanOrEqual(2);
  477. // Check nodes were stored
  478. const nodes = cg.getNodesInFile('src/utils.ts');
  479. expect(nodes.length).toBeGreaterThanOrEqual(2);
  480. const addFunc = nodes.find((n) => n.name === 'add');
  481. expect(addFunc).toBeDefined();
  482. expect(addFunc?.kind).toBe('function');
  483. cg.close();
  484. });
  485. it('should index multiple files', async () => {
  486. // Create test files
  487. const srcDir = path.join(tempDir, 'src');
  488. fs.mkdirSync(srcDir);
  489. fs.writeFileSync(
  490. path.join(srcDir, 'math.ts'),
  491. `export function add(a: number, b: number) { return a + b; }`
  492. );
  493. fs.writeFileSync(
  494. path.join(srcDir, 'string.ts'),
  495. `export function capitalize(s: string) { return s.toUpperCase(); }`
  496. );
  497. // Initialize and index
  498. const cg = CodeGraph.initSync(tempDir);
  499. const result = await cg.indexAll();
  500. expect(result.success).toBe(true);
  501. expect(result.filesIndexed).toBe(2);
  502. const files = cg.getFiles();
  503. expect(files.length).toBe(2);
  504. cg.close();
  505. });
  506. it('should track file hashes for incremental updates', async () => {
  507. // Create initial file
  508. const srcDir = path.join(tempDir, 'src');
  509. fs.mkdirSync(srcDir);
  510. fs.writeFileSync(path.join(srcDir, 'main.ts'), `export const x = 1;`);
  511. // Initialize and index
  512. const cg = CodeGraph.initSync(tempDir);
  513. await cg.indexAll();
  514. // Check file is tracked
  515. const file = cg.getFile('src/main.ts');
  516. expect(file).toBeDefined();
  517. expect(file?.contentHash).toBeDefined();
  518. // Modify file
  519. fs.writeFileSync(path.join(srcDir, 'main.ts'), `export const x = 2;`);
  520. // Check for changes
  521. const changes = cg.getChangedFiles();
  522. expect(changes.modified).toContain('src/main.ts');
  523. cg.close();
  524. });
  525. it('should sync and detect changes', async () => {
  526. // Create initial file
  527. const srcDir = path.join(tempDir, 'src');
  528. fs.mkdirSync(srcDir);
  529. fs.writeFileSync(
  530. path.join(srcDir, 'main.ts'),
  531. `export function original() { return 1; }`
  532. );
  533. // Initialize and index
  534. const cg = CodeGraph.initSync(tempDir);
  535. await cg.indexAll();
  536. const initialNodes = cg.getNodesInFile('src/main.ts');
  537. expect(initialNodes.some((n) => n.name === 'original')).toBe(true);
  538. // Modify file
  539. fs.writeFileSync(
  540. path.join(srcDir, 'main.ts'),
  541. `export function updated() { return 2; }`
  542. );
  543. // Sync
  544. const syncResult = await cg.sync();
  545. expect(syncResult.filesModified).toBe(1);
  546. // Check nodes were updated
  547. const updatedNodes = cg.getNodesInFile('src/main.ts');
  548. expect(updatedNodes.some((n) => n.name === 'updated')).toBe(true);
  549. expect(updatedNodes.some((n) => n.name === 'original')).toBe(false);
  550. cg.close();
  551. });
  552. });