1mod entity_extractor;
2mod languages;
3
4use std::cell::RefCell;
5use std::collections::HashMap;
6
7use crate::model::entity::SemanticEntity;
8use crate::parser::plugin::SemanticParserPlugin;
9use languages::{get_all_code_extensions, get_language_config};
10use entity_extractor::extract_entities;
11
12pub struct CodeParserPlugin;
13
14thread_local! {
17 static PARSER_CACHE: RefCell<HashMap<&'static str, tree_sitter::Parser>> = RefCell::new(HashMap::new());
18}
19
20impl SemanticParserPlugin for CodeParserPlugin {
21 fn id(&self) -> &str {
22 "code"
23 }
24
25 fn extensions(&self) -> &[&str] {
26 get_all_code_extensions()
27 }
28
29 fn extract_entities(&self, content: &str, file_path: &str) -> Vec<SemanticEntity> {
30 let ext = std::path::Path::new(file_path)
31 .extension()
32 .and_then(|e| e.to_str())
33 .map(|e| format!(".{}", e.to_lowercase()))
34 .unwrap_or_default();
35
36 let config = match get_language_config(&ext) {
37 Some(c) => c,
38 None => return Vec::new(),
39 };
40
41 let language = match (config.get_language)() {
42 Some(lang) => lang,
43 None => return Vec::new(),
44 };
45
46 PARSER_CACHE.with(|cache| {
47 let mut cache = cache.borrow_mut();
48 let parser = cache.entry(config.id).or_insert_with(|| {
49 let mut p = tree_sitter::Parser::new();
50 let _ = p.set_language(&language);
51 p
52 });
53
54 let tree = match parser.parse(content.as_bytes(), None) {
55 Some(t) => t,
56 None => return Vec::new(),
57 };
58
59 extract_entities(&tree, file_path, config, content)
60 })
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn test_java_entity_extraction() {
70 let code = r#"
71package com.example;
72
73import java.util.List;
74
75public class UserService {
76 private String name;
77
78 public UserService(String name) {
79 this.name = name;
80 }
81
82 public List<User> getUsers() {
83 return db.findAll();
84 }
85
86 public void createUser(User user) {
87 db.save(user);
88 }
89}
90
91interface Repository<T> {
92 T findById(String id);
93 List<T> findAll();
94}
95
96enum Status {
97 ACTIVE,
98 INACTIVE,
99 DELETED
100}
101"#;
102 let plugin = CodeParserPlugin;
103 let entities = plugin.extract_entities(code, "UserService.java");
104 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
105 let types: Vec<&str> = entities.iter().map(|e| e.entity_type.as_str()).collect();
106 eprintln!("Java entities: {:?}", names.iter().zip(types.iter()).collect::<Vec<_>>());
107
108 assert!(names.contains(&"UserService"), "Should find class UserService, got: {:?}", names);
109 assert!(names.contains(&"Repository"), "Should find interface Repository, got: {:?}", names);
110 assert!(names.contains(&"Status"), "Should find enum Status, got: {:?}", names);
111 }
112
113 #[test]
114 fn test_java_nested_methods() {
115 let code = r#"
116public class Calculator {
117 public int add(int a, int b) {
118 return a + b;
119 }
120
121 public int subtract(int a, int b) {
122 return a - b;
123 }
124}
125"#;
126 let plugin = CodeParserPlugin;
127 let entities = plugin.extract_entities(code, "Calculator.java");
128 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
129 eprintln!("Java nested: {:?}", entities.iter().map(|e| (&e.name, &e.entity_type, &e.parent_id)).collect::<Vec<_>>());
130
131 assert!(names.contains(&"Calculator"), "Should find Calculator class");
132 assert!(names.contains(&"add"), "Should find add method, got: {:?}", names);
133 assert!(names.contains(&"subtract"), "Should find subtract method, got: {:?}", names);
134
135 let add = entities.iter().find(|e| e.name == "add").unwrap();
137 assert!(add.parent_id.is_some(), "add should have parent_id");
138 }
139
140 #[test]
141 fn test_c_entity_extraction() {
142 let code = r#"
143#include <stdio.h>
144
145struct Point {
146 int x;
147 int y;
148};
149
150enum Color {
151 RED,
152 GREEN,
153 BLUE
154};
155
156typedef struct {
157 char name[50];
158 int age;
159} Person;
160
161void greet(const char* name) {
162 printf("Hello, %s!\n", name);
163}
164
165int add(int a, int b) {
166 return a + b;
167}
168
169int main() {
170 greet("world");
171 return 0;
172}
173"#;
174 let plugin = CodeParserPlugin;
175 let entities = plugin.extract_entities(code, "main.c");
176 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
177 let types: Vec<&str> = entities.iter().map(|e| e.entity_type.as_str()).collect();
178 eprintln!("C entities: {:?}", names.iter().zip(types.iter()).collect::<Vec<_>>());
179
180 assert!(names.contains(&"greet"), "Should find greet function, got: {:?}", names);
181 assert!(names.contains(&"add"), "Should find add function, got: {:?}", names);
182 assert!(names.contains(&"main"), "Should find main function, got: {:?}", names);
183 assert!(names.contains(&"Point"), "Should find Point struct, got: {:?}", names);
184 assert!(names.contains(&"Color"), "Should find Color enum, got: {:?}", names);
185 }
186
187 #[test]
188 fn test_cpp_entity_extraction() {
189 let code = "namespace math {\nclass Vector3 {\npublic:\n float length() const { return 0; }\n};\n}\nvoid greet() {}\n";
190 let plugin = CodeParserPlugin;
191 let entities = plugin.extract_entities(code, "main.cpp");
192 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
193 assert!(names.contains(&"math"), "got: {:?}", names);
194 assert!(names.contains(&"Vector3"), "got: {:?}", names);
195 assert!(names.contains(&"greet"), "got: {:?}", names);
196 }
197
198 #[test]
199 fn test_ruby_entity_extraction() {
200 let code = "module Auth\n class User\n def greet\n \"hi\"\n end\n end\nend\ndef helper(x)\n x * 2\nend\n";
201 let plugin = CodeParserPlugin;
202 let entities = plugin.extract_entities(code, "auth.rb");
203 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
204 assert!(names.contains(&"Auth"), "got: {:?}", names);
205 assert!(names.contains(&"User"), "got: {:?}", names);
206 assert!(names.contains(&"helper"), "got: {:?}", names);
207 }
208
209 #[test]
210 fn test_csharp_entity_extraction() {
211 let code = "namespace MyApp {\npublic class User {\n public string GetName() { return \"\"; }\n}\npublic enum Role { Admin, User }\n}\n";
212 let plugin = CodeParserPlugin;
213 let entities = plugin.extract_entities(code, "Models.cs");
214 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
215 assert!(names.contains(&"MyApp"), "got: {:?}", names);
216 assert!(names.contains(&"User"), "got: {:?}", names);
217 assert!(names.contains(&"Role"), "got: {:?}", names);
218 }
219
220 #[test]
221 fn test_swift_entity_extraction() {
222 let code = r#"
223import Foundation
224
225class UserService {
226 var name: String
227
228 init(name: String) {
229 self.name = name
230 }
231
232 func getUsers() -> [User] {
233 return db.findAll()
234 }
235}
236
237struct Point {
238 var x: Double
239 var y: Double
240}
241
242enum Status {
243 case active
244 case inactive
245 case deleted
246}
247
248protocol Repository {
249 associatedtype Item
250 func findById(id: String) -> Item?
251 func findAll() -> [Item]
252}
253
254func helper(x: Int) -> Int {
255 return x * 2
256}
257"#;
258 let plugin = CodeParserPlugin;
259 let entities = plugin.extract_entities(code, "UserService.swift");
260 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
261 eprintln!("Swift entities: {:?}", entities.iter().map(|e| (&e.name, &e.entity_type)).collect::<Vec<_>>());
262
263 assert!(names.contains(&"UserService"), "Should find class UserService, got: {:?}", names);
264 assert!(names.contains(&"Point"), "Should find struct Point, got: {:?}", names);
265 assert!(names.contains(&"Status"), "Should find enum Status, got: {:?}", names);
266 assert!(names.contains(&"Repository"), "Should find protocol Repository, got: {:?}", names);
267 assert!(names.contains(&"helper"), "Should find function helper, got: {:?}", names);
268 }
269
270 #[test]
271 fn test_elixir_entity_extraction() {
272 let code = r#"
273defmodule MyApp.Accounts do
274 def create_user(attrs) do
275 %User{}
276 |> User.changeset(attrs)
277 |> Repo.insert()
278 end
279
280 defp validate(attrs) do
281 # private helper
282 :ok
283 end
284
285 defmacro is_admin(user) do
286 quote do
287 unquote(user).role == :admin
288 end
289 end
290
291 defguard is_positive(x) when is_integer(x) and x > 0
292end
293
294defprotocol Printable do
295 def to_string(data)
296end
297
298defimpl Printable, for: Integer do
299 def to_string(i), do: Integer.to_string(i)
300end
301"#;
302 let plugin = CodeParserPlugin;
303 let entities = plugin.extract_entities(code, "accounts.ex");
304 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
305 let types: Vec<&str> = entities.iter().map(|e| e.entity_type.as_str()).collect();
306 eprintln!("Elixir entities: {:?}", names.iter().zip(types.iter()).collect::<Vec<_>>());
307
308 assert!(names.contains(&"MyApp.Accounts"), "Should find module, got: {:?}", names);
309 assert!(names.contains(&"create_user"), "Should find def, got: {:?}", names);
310 assert!(names.contains(&"validate"), "Should find defp, got: {:?}", names);
311 assert!(names.contains(&"is_admin"), "Should find defmacro, got: {:?}", names);
312 assert!(names.contains(&"Printable"), "Should find defprotocol, got: {:?}", names);
313
314 let create_user = entities.iter().find(|e| e.name == "create_user").unwrap();
316 assert!(create_user.parent_id.is_some(), "create_user should be nested under module");
317 }
318
319 #[test]
320 fn test_bash_entity_extraction() {
321 let code = r#"#!/bin/bash
322
323greet() {
324 echo "Hello, $1!"
325}
326
327function deploy {
328 echo "deploying..."
329}
330
331# not a function
332echo "main script"
333"#;
334 let plugin = CodeParserPlugin;
335 let entities = plugin.extract_entities(code, "deploy.sh");
336 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
337 let types: Vec<&str> = entities.iter().map(|e| e.entity_type.as_str()).collect();
338 eprintln!("Bash entities: {:?}", names.iter().zip(types.iter()).collect::<Vec<_>>());
339
340 assert!(names.contains(&"greet"), "Should find greet(), got: {:?}", names);
341 assert!(names.contains(&"deploy"), "Should find function deploy, got: {:?}", names);
342 assert_eq!(entities.len(), 2, "Should only find functions, got: {:?}", names);
343 }
344
345 #[test]
346 fn test_typescript_entity_extraction() {
347 let code = r#"
349export function hello(): string {
350 return "hello";
351}
352
353export class Greeter {
354 greet(name: string): string {
355 return `Hello, ${name}!`;
356 }
357}
358"#;
359 let plugin = CodeParserPlugin;
360 let entities = plugin.extract_entities(code, "test.ts");
361 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
362 assert!(names.contains(&"hello"), "Should find hello function");
363 assert!(names.contains(&"Greeter"), "Should find Greeter class");
364 }
365
366 #[test]
367 fn test_module_typescript_entity_extraction() {
368 let code = r#"
369export function hello(): string {
370 return "hello";
371}
372"#;
373 let plugin = CodeParserPlugin;
374 let entities = plugin.extract_entities(code, "test.mts");
375 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
376
377 assert!(names.contains(&"hello"), "Should find hello function");
378 }
379
380 #[test]
381 fn test_commonjs_typescript_entity_extraction() {
382 let code = r#"
383export class Greeter {
384 greet(name: string): string {
385 return `Hello, ${name}!`;
386 }
387}
388"#;
389 let plugin = CodeParserPlugin;
390 let entities = plugin.extract_entities(code, "test.cts");
391 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
392
393 assert!(names.contains(&"Greeter"), "Should find Greeter class");
394 assert!(names.contains(&"greet"), "Should find greet method");
395 }
396
397 #[test]
398 fn test_nested_functions_typescript() {
399 let code = r#"
400function outer() {
401 function inner() {
402 return 42;
403 }
404 return inner();
405}
406"#;
407 let plugin = CodeParserPlugin;
408 let entities = plugin.extract_entities(code, "nested.ts");
409 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
410 eprintln!("Nested TS: {:?}", entities.iter().map(|e| (&e.name, &e.entity_type, &e.parent_id)).collect::<Vec<_>>());
411
412 assert!(names.contains(&"outer"), "Should find outer, got: {:?}", names);
413 assert!(names.contains(&"inner"), "Should find inner, got: {:?}", names);
414
415 let inner = entities.iter().find(|e| e.name == "inner").unwrap();
416 assert!(inner.parent_id.is_some(), "inner should have parent_id");
417 }
418
419 #[test]
420 fn test_nested_functions_python() {
421 let code = "def outer():\n def inner():\n return 42\n return inner()\n";
422 let plugin = CodeParserPlugin;
423 let entities = plugin.extract_entities(code, "nested.py");
424 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
425
426 assert!(names.contains(&"outer"), "got: {:?}", names);
427 assert!(names.contains(&"inner"), "got: {:?}", names);
428
429 let inner = entities.iter().find(|e| e.name == "inner").unwrap();
430 assert!(inner.parent_id.is_some(), "inner should have parent_id");
431 }
432
433 #[test]
434 fn test_nested_functions_rust() {
435 let code = "fn outer() {\n fn inner() -> i32 {\n 42\n }\n inner();\n}\n";
436 let plugin = CodeParserPlugin;
437 let entities = plugin.extract_entities(code, "nested.rs");
438 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
439
440 assert!(names.contains(&"outer"), "got: {:?}", names);
441 assert!(names.contains(&"inner"), "got: {:?}", names);
442
443 let inner = entities.iter().find(|e| e.name == "inner").unwrap();
444 assert!(inner.parent_id.is_some(), "inner should have parent_id");
445 }
446
447 #[test]
448 fn test_rust_impl_blocks_unique_names() {
449 let code = r#"
450trait Greeting {
451 fn greet(&self) -> String;
452}
453
454struct Person;
455struct Robot;
456struct Cat;
457
458impl Greeting for Person {
459 fn greet(&self) -> String { "Hello".to_string() }
460}
461
462impl Greeting for Robot {
463 fn greet(&self) -> String { "Beep".to_string() }
464}
465
466impl Greeting for Cat {
467 fn greet(&self) -> String { "Meow".to_string() }
468}
469"#;
470 let plugin = CodeParserPlugin;
471 let entities = plugin.extract_entities(code, "impls.rs");
472 let impl_entities: Vec<&_> = entities.iter()
473 .filter(|e| e.entity_type == "impl")
474 .collect();
475 let names: Vec<&str> = impl_entities.iter().map(|e| e.name.as_str()).collect();
476
477 assert_eq!(impl_entities.len(), 3, "Should find 3 impl blocks, got: {:?}", names);
478 assert!(names.contains(&"Greeting for Person"), "got: {:?}", names);
479 assert!(names.contains(&"Greeting for Robot"), "got: {:?}", names);
480 assert!(names.contains(&"Greeting for Cat"), "got: {:?}", names);
481 }
482
483 #[test]
484 fn test_nested_functions_go() {
485 let code = "package main\n\nfunc outer() {\n var x int = 42\n _ = x\n}\n";
487 let plugin = CodeParserPlugin;
488 let entities = plugin.extract_entities(code, "nested.go");
489 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
490
491 assert!(names.contains(&"outer"), "got: {:?}", names);
492 }
493
494 #[test]
495 fn test_renamed_function_same_structural_hash() {
496 let code_a = "def get_card():\n return db.query('cards')\n";
497 let code_b = "def get_card_1():\n return db.query('cards')\n";
498
499 let plugin = CodeParserPlugin;
500 let entities_a = plugin.extract_entities(code_a, "a.py");
501 let entities_b = plugin.extract_entities(code_b, "b.py");
502
503 assert_eq!(entities_a.len(), 1, "Should find one entity in a");
504 assert_eq!(entities_b.len(), 1, "Should find one entity in b");
505 assert_eq!(entities_a[0].name, "get_card");
506 assert_eq!(entities_b[0].name, "get_card_1");
507
508 assert_eq!(
510 entities_a[0].structural_hash, entities_b[0].structural_hash,
511 "Renamed function with identical body should have same structural_hash"
512 );
513
514 assert_ne!(
516 entities_a[0].content_hash, entities_b[0].content_hash,
517 "Content hash should differ since raw content includes the name"
518 );
519 }
520
521 #[test]
522 fn test_hcl_entity_extraction() {
523 let code = r#"
524region = "eu-west-1"
525
526variable "image_id" {
527 type = string
528}
529
530resource "aws_instance" "web" {
531 ami = var.image_id
532
533 lifecycle {
534 create_before_destroy = true
535 }
536}
537"#;
538 let plugin = CodeParserPlugin;
539 let entities = plugin.extract_entities(code, "main.tf");
540 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
541 let types: Vec<&str> = entities.iter().map(|e| e.entity_type.as_str()).collect();
542 eprintln!("HCL entities: {:?}", entities.iter().map(|e| (&e.name, &e.entity_type, &e.parent_id)).collect::<Vec<_>>());
543
544 assert!(names.contains(&"region"), "Should find top-level attribute, got: {:?}", names);
545 assert!(names.contains(&"variable.image_id"), "Should find variable block, got: {:?}", names);
546 assert!(names.contains(&"resource.aws_instance.web"), "Should find resource block, got: {:?}", names);
547 assert!(
548 names.contains(&"resource.aws_instance.web.lifecycle"),
549 "Should find nested lifecycle block with qualified name, got: {:?}",
550 names
551 );
552 assert!(!names.contains(&"ami"), "Should skip nested attributes inside blocks, got: {:?}", names);
553 assert!(
554 !names.contains(&"create_before_destroy"),
555 "Should skip nested attributes inside nested blocks, got: {:?}",
556 names
557 );
558
559 let lifecycle = entities
560 .iter()
561 .find(|e| e.name == "resource.aws_instance.web.lifecycle")
562 .unwrap();
563 assert!(lifecycle.parent_id.is_some(), "lifecycle should be nested under resource");
564 assert!(types.contains(&"attribute"), "Should preserve attribute entity type for top-level attributes");
565 }
566
567 #[test]
568 fn test_kotlin_entity_extraction() {
569 let code = r#"
570class UserService {
571 val name: String = ""
572
573 fun greet(): String {
574 return "Hello, $name"
575 }
576
577 companion object {
578 fun create(): UserService = UserService()
579 }
580}
581
582interface Repository {
583 fun findById(id: Int): Any?
584}
585
586object AppConfig {
587 val version = "1.0"
588}
589
590fun topLevel(x: Int): Int = x * 2
591"#;
592 let plugin = CodeParserPlugin;
593 let entities = plugin.extract_entities(code, "App.kt");
594 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
595 eprintln!("Kotlin entities: {:?}", entities.iter().map(|e| (&e.name, &e.entity_type)).collect::<Vec<_>>());
596 assert!(names.contains(&"UserService"), "got: {:?}", names);
597 assert!(names.contains(&"greet"), "got: {:?}", names);
598 assert!(names.contains(&"Repository"), "got: {:?}", names);
599 assert!(names.contains(&"findById"), "got: {:?}", names);
600 assert!(names.contains(&"AppConfig"), "got: {:?}", names);
601 assert!(names.contains(&"topLevel"), "got: {:?}", names);
602 }
603
604 #[test]
605 fn test_xml_entity_extraction() {
606 let code = r#"<?xml version="1.0" encoding="UTF-8"?>
607<project>
608 <groupId>com.example</groupId>
609 <artifactId>my-app</artifactId>
610 <dependencies>
611 <dependency>
612 <groupId>junit</groupId>
613 <artifactId>junit</artifactId>
614 </dependency>
615 </dependencies>
616 <build>
617 <plugins>
618 <plugin>
619 <groupId>org.apache.maven</groupId>
620 </plugin>
621 </plugins>
622 </build>
623</project>
624"#;
625 let plugin = CodeParserPlugin;
626 let entities = plugin.extract_entities(code, "pom.xml");
627 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
628 eprintln!("XML entities: {:?}", entities.iter().map(|e| (&e.name, &e.entity_type)).collect::<Vec<_>>());
629 assert!(names.contains(&"project"), "got: {:?}", names);
630 assert!(names.contains(&"dependencies"), "got: {:?}", names);
631 assert!(names.contains(&"build"), "got: {:?}", names);
632 }
633
634 #[test]
635 fn test_arrow_callback_scope_boundary_typescript() {
636 let code = r#"
640const activeQueues = [
641 { queue: queues.fooQueue, processor: foo.process },
642];
643
644activeQueues.forEach((handler: any) => {
645 const queue = handler.queue;
646 let retries = 0;
647
648 class QueueHandler {
649 handle() { return queue; }
650 }
651
652 function createHandler() {
653 return new QueueHandler();
654 }
655
656 queue.process((job) => {
657 const orderId = job.data.orderId;
658 return orderId;
659 });
660});
661
662function handleFailure(job: any, err: any) {
663 console.error('failed', err);
664}
665"#;
666 let plugin = CodeParserPlugin;
667 let entities = plugin.extract_entities(code, "process.ts");
668 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
669 let top_level: Vec<&str> = entities
670 .iter()
671 .filter(|e| e.parent_id.is_none())
672 .map(|e| e.name.as_str())
673 .collect();
674
675 assert!(top_level.contains(&"activeQueues"), "got: {:?}", top_level);
677 assert!(top_level.contains(&"handleFailure"), "got: {:?}", top_level);
678
679 assert!(names.contains(&"QueueHandler"), "got: {:?}", names);
681 assert!(names.contains(&"handle"), "got: {:?}", names);
682 assert!(names.contains(&"createHandler"), "got: {:?}", names);
683
684 assert!(!names.contains(&"queue"), "got: {:?}", names);
686 assert!(!names.contains(&"retries"), "got: {:?}", names);
687 assert!(!names.contains(&"orderId"), "got: {:?}", names);
688 }
689
690 #[test]
691 fn test_top_level_iife_wrapper_still_extracts_typescript_entities() {
692 let code = r#"
693function factory() {
694 class Foo {
695 method(): number {
696 return 1;
697 }
698 }
699
700 function bar(): Foo {
701 return new Foo();
702 }
703}
704
705factory();
706"#;
707 let plugin = CodeParserPlugin;
708 let entities = plugin.extract_entities(code, "wrapped.ts");
709 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
710 assert!(
711 names.contains(&"factory"),
712 "Should find top-level wrapper function, got: {:?}",
713 names
714 );
715 assert!(
716 names.contains(&"Foo"),
717 "Should find class inside top-level wrapper, got: {:?}",
718 names
719 );
720 assert!(
721 names.contains(&"bar"),
722 "Should find function inside top-level wrapper, got: {:?}",
723 names
724 );
725 }
726
727 #[test]
728 fn test_top_level_iife_still_extracts_typescript_entities() {
729 let code = r#"
730(() => {
731 class Foo {
732 method(): number {
733 return 1;
734 }
735 }
736
737 function bar(): Foo {
738 return new Foo();
739 }
740})();
741"#;
742 let plugin = CodeParserPlugin;
743 let entities = plugin.extract_entities(code, "iife.ts");
744 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
745 assert!(
746 names.contains(&"Foo"),
747 "Should find class inside top-level IIFE, got: {:?}",
748 names
749 );
750 assert!(
751 names.contains(&"bar"),
752 "Should find function inside top-level IIFE, got: {:?}",
753 names
754 );
755 }
756
757 #[test]
758 fn test_function_locals_not_extracted_as_nested_entities_typescript() {
759 let code = r#"
760export default function foo() {
761 const x = 1;
762 return x;
763}
764"#;
765 let plugin = CodeParserPlugin;
766 let entities = plugin.extract_entities(code, "default-export.ts");
767 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
768 assert!(
769 names.contains(&"foo"),
770 "Should find exported function, got: {:?}",
771 names
772 );
773 assert!(
774 !names.contains(&"x"),
775 "Local inside function should not be extracted as an entity, got: {:?}",
776 names
777 );
778 }
779
780 #[test]
781 fn test_function_expression_scope_boundary_typescript() {
782 let code = r#"
785const foo = function namedExpr(x: number) {
786 const inner = x + 1;
787 return inner;
788};
789
790const bar = function(y: number) {
791 const local = y * 2;
792 return local;
793};
794
795const items = [1, 2, 3];
796
797items.forEach(function process(item) {
798 const doubled = item * 2;
799 console.log(doubled);
800});
801"#;
802 let plugin = CodeParserPlugin;
803 let entities = plugin.extract_entities(code, "funexpr.ts");
804 let top_level: Vec<&str> = entities
805 .iter()
806 .filter(|e| e.parent_id.is_none())
807 .map(|e| e.name.as_str())
808 .collect();
809 let all_names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
810
811 assert!(top_level.contains(&"foo"), "got: {:?}", top_level);
813 assert!(top_level.contains(&"bar"), "got: {:?}", top_level);
814 assert!(top_level.contains(&"items"), "got: {:?}", top_level);
815
816 assert!(!all_names.contains(&"inner"), "got: {:?}", all_names);
818 assert!(!all_names.contains(&"local"), "got: {:?}", all_names);
819 assert!(!all_names.contains(&"doubled"), "got: {:?}", all_names);
820
821 assert!(!top_level.contains(&"process"), "got: {:?}", top_level);
823 }
824
825 #[test]
826 fn test_variable_assigned_arrow_extracts_inner_entities() {
827 let code = r#"
830const handler = () => {
831 class Inner {
832 run() { return 1; }
833 }
834
835 function make() {
836 return new Inner();
837 }
838
839 const local = 42;
840};
841"#;
842 let plugin = CodeParserPlugin;
843 let entities = plugin.extract_entities(code, "assigned.ts");
844 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
845
846 assert!(names.contains(&"handler"), "got: {:?}", names);
847 assert!(names.contains(&"Inner"), "got: {:?}", names);
848 assert!(names.contains(&"run"), "got: {:?}", names);
849 assert!(names.contains(&"make"), "got: {:?}", names);
850 assert!(!names.contains(&"local"), "got: {:?}", names);
851 }
852
853 #[test]
854 fn test_variable_assigned_function_expression_extracts_inner_entities() {
855 let code = r#"
857const handler = function() {
858 class Inner {}
859 function make() { return new Inner(); }
860 const local = 42;
861};
862"#;
863 let plugin = CodeParserPlugin;
864 let entities = plugin.extract_entities(code, "funexpr-inner.ts");
865 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
866
867 assert!(names.contains(&"handler"), "got: {:?}", names);
868 assert!(names.contains(&"Inner"), "got: {:?}", names);
869 assert!(names.contains(&"make"), "got: {:?}", names);
870 assert!(!names.contains(&"local"), "got: {:?}", names);
871 }
872
873 #[test]
874 fn test_go_var_declaration() {
875 let code = r#"package featuremgmt
876
877type FeatureFlag struct {
878 Name string
879 Description string
880 Stage string
881}
882
883var standardFeatureFlags = []FeatureFlag{
884 {
885 Name: "panelTitleSearch",
886 Description: "Search for dashboards using panel title",
887 Stage: "PublicPreview",
888 },
889}
890
891func GetFlags() []FeatureFlag {
892 return standardFeatureFlags
893}
894"#;
895 let plugin = CodeParserPlugin;
896 let entities = plugin.extract_entities(code, "flags.go");
897 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
898 let types: Vec<&str> = entities.iter().map(|e| e.entity_type.as_str()).collect();
899 eprintln!("Go entities: {:?}", names.iter().zip(types.iter()).collect::<Vec<_>>());
900
901 assert!(names.contains(&"FeatureFlag"), "Should find type FeatureFlag, got: {:?}", names);
902 assert!(names.contains(&"standardFeatureFlags"), "Should find var standardFeatureFlags, got: {:?}", names);
903 assert!(names.contains(&"GetFlags"), "Should find func GetFlags, got: {:?}", names);
904 }
905
906 #[test]
907 fn test_go_grouped_var_declaration() {
908 let code = r#"package test
909
910var (
911 simple = 42
912 flags = []string{"a", "b"}
913)
914
915const (
916 x = 1
917 y = 2
918)
919
920func main() {}
921"#;
922 let plugin = CodeParserPlugin;
923 let entities = plugin.extract_entities(code, "test.go");
924 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
925 let types: Vec<&str> = entities.iter().map(|e| e.entity_type.as_str()).collect();
926 eprintln!("Go grouped entities: {:?}", names.iter().zip(types.iter()).collect::<Vec<_>>());
927
928 assert!(names.contains(&"flags") || names.contains(&"simple"), "Should find grouped var, got: {:?}", names);
929 assert!(names.contains(&"x"), "Should find grouped const x, got: {:?}", names);
930 assert!(names.contains(&"main"), "Should find func main, got: {:?}", names);
931 }
932
933 #[test]
934 fn test_dart_entity_extraction() {
935 let code = r#"
936import 'dart:math';
937
938class Calculator {
939 final String name;
940
941 Calculator(this.name);
942
943 Calculator.withDefault() : name = 'default';
944
945 factory Calculator.create(String name) {
946 return Calculator(name);
947 }
948
949 int add(int a, int b) {
950 return a + b;
951 }
952
953 int get doubleAdd => add(1, 1) * 2;
954
955 set label(String value) {
956 // no-op
957 }
958
959 int operator +(Calculator other) {
960 return 0;
961 }
962}
963
964mixin Loggable {
965 void log(String message) {
966 print(message);
967 }
968}
969
970extension StringExt on String {
971 bool get isBlank => trim().isEmpty;
972}
973
974enum Status {
975 active,
976 inactive;
977
978 String display() => name.toUpperCase();
979}
980
981typedef Callback = void Function(int);
982
983int add(int a, int b) {
984 return a + b;
985}
986
987extension type Wrapper(int value) implements int {}
988"#;
989 let plugin = CodeParserPlugin;
990 let entities = plugin.extract_entities(code, "calculator.dart");
991 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
992 eprintln!(
993 "Dart entities: {:?}",
994 entities
995 .iter()
996 .map(|e| (&e.name, &e.entity_type, &e.parent_id))
997 .collect::<Vec<_>>()
998 );
999
1000 assert!(names.contains(&"Calculator"), "Should find class, got: {:?}", names);
1002 assert!(names.contains(&"Loggable"), "Should find mixin, got: {:?}", names);
1003 assert!(names.contains(&"StringExt"), "Should find extension, got: {:?}", names);
1004 assert!(names.contains(&"Status"), "Should find enum, got: {:?}", names);
1005 assert!(names.contains(&"Callback"), "Should find typedef, got: {:?}", names);
1006 assert!(names.contains(&"add"), "Should find top-level function, got: {:?}", names);
1007 assert!(names.contains(&"Wrapper"), "Should find extension type, got: {:?}", names);
1008
1009 let add_method = entities.iter().find(|e| e.name == "add" && e.parent_id.is_some());
1011 assert!(add_method.is_some(), "Should find add method inside Calculator");
1012 assert_eq!(add_method.unwrap().entity_type, "method");
1013
1014 let unnamed_ctor = entities.iter().find(|e| e.name == "Calculator" && e.entity_type == "constructor");
1016 assert!(unnamed_ctor.is_some(), "Should find unnamed constructor");
1017 let named_ctor = entities.iter().find(|e| e.name == "Calculator.withDefault");
1018 assert!(named_ctor.is_some(), "Should find named constructor Calculator.withDefault, got: {:?}", names);
1019 assert_eq!(named_ctor.unwrap().entity_type, "constructor");
1020 assert_ne!(unnamed_ctor.unwrap().id, named_ctor.unwrap().id, "Named and unnamed constructors must have different entity IDs");
1021
1022 let factory_ctor = entities.iter().find(|e| e.name == "Calculator.create");
1024 assert!(factory_ctor.is_some(), "Should find factory constructor Calculator.create, got: {:?}", names);
1025 assert_eq!(factory_ctor.unwrap().entity_type, "constructor");
1026
1027 let getter = entities.iter().find(|e| e.name == "doubleAdd");
1029 assert!(getter.is_some(), "Should find getter doubleAdd");
1030 assert_eq!(getter.unwrap().entity_type, "getter");
1031
1032 let setter = entities.iter().find(|e| e.name == "label");
1033 assert!(setter.is_some(), "Should find setter label");
1034 assert_eq!(setter.unwrap().entity_type, "setter");
1035
1036 let operator = entities.iter().find(|e| e.name == "operator +");
1037 assert!(operator.is_some(), "Should find operator +");
1038 assert_eq!(operator.unwrap().entity_type, "method");
1039
1040 let log_method = entities.iter().find(|e| e.name == "log");
1042 assert!(log_method.is_some(), "Should find log in Loggable");
1043 assert!(log_method.unwrap().parent_id.is_some(), "log should have parent_id");
1044
1045 let callback = entities.iter().find(|e| e.name == "Callback").unwrap();
1047 assert_eq!(callback.entity_type, "type", "typedef should map to 'type'");
1048
1049 let loggable = entities.iter().find(|e| e.name == "Loggable").unwrap();
1050 assert_eq!(loggable.entity_type, "mixin");
1051
1052 let ext = entities.iter().find(|e| e.name == "StringExt").unwrap();
1053 assert_eq!(ext.entity_type, "extension");
1054
1055 let wrapper = entities.iter().find(|e| e.name == "Wrapper").unwrap();
1056 assert_eq!(wrapper.entity_type, "extension");
1057 }
1058
1059 #[test]
1060 fn test_dart_top_level_function_includes_body() {
1061 let code = r#"
1062int add(int a, int b) {
1063 return a + b;
1064}
1065
1066String greet(String name) => 'Hello, $name!';
1067"#;
1068 let plugin = CodeParserPlugin;
1069 let entities = plugin.extract_entities(code, "funcs.dart");
1070 eprintln!(
1071 "Dart top-level: {:?}",
1072 entities
1073 .iter()
1074 .map(|e| (&e.name, &e.entity_type, &e.content))
1075 .collect::<Vec<_>>()
1076 );
1077
1078 let add_fn = entities.iter().find(|e| e.name == "add").unwrap();
1079 assert!(
1080 add_fn.content.contains("return a + b"),
1081 "Top-level function content should include the body, got: {:?}",
1082 add_fn.content
1083 );
1084
1085 let greet_fn = entities.iter().find(|e| e.name == "greet").unwrap();
1086 assert!(
1087 greet_fn.content.contains("Hello"),
1088 "Expression body should be included, got: {:?}",
1089 greet_fn.content
1090 );
1091
1092 let code_v2 = r#"
1094int add(int a, int b) {
1095 return a * b;
1096}
1097
1098String greet(String name) => 'Hello, $name!';
1099"#;
1100 let entities_v2 = plugin.extract_entities(code_v2, "funcs.dart");
1101 let add_v2 = entities_v2.iter().find(|e| e.name == "add").unwrap();
1102 assert_ne!(
1103 add_fn.content_hash, add_v2.content_hash,
1104 "Body change should produce different content_hash"
1105 );
1106
1107 let greet_v2 = entities_v2.iter().find(|e| e.name == "greet").unwrap();
1109 assert_eq!(
1110 greet_fn.content_hash, greet_v2.content_hash,
1111 "Unchanged function should keep the same content_hash"
1112 );
1113 }
1114
1115 #[test]
1116 fn test_dart_renamed_named_constructor_same_structural_hash() {
1117 let code_a = r#"
1118class Foo {
1119 Foo.fromJson(Map<String, dynamic> json) {
1120 print(json);
1121 }
1122}
1123"#;
1124 let code_b = r#"
1125class Foo {
1126 Foo.fromMap(Map<String, dynamic> json) {
1127 print(json);
1128 }
1129}
1130"#;
1131 let plugin = CodeParserPlugin;
1132 let entities_a = plugin.extract_entities(code_a, "a.dart");
1133 let entities_b = plugin.extract_entities(code_b, "b.dart");
1134
1135 let ctor_a = entities_a.iter().find(|e| e.name == "Foo.fromJson").unwrap();
1136 let ctor_b = entities_b.iter().find(|e| e.name == "Foo.fromMap").unwrap();
1137
1138 assert_eq!(
1139 ctor_a.structural_hash, ctor_b.structural_hash,
1140 "Renamed named constructor with identical body should have same structural_hash"
1141 );
1142 assert_ne!(
1143 ctor_a.content_hash, ctor_b.content_hash,
1144 "Content hash should differ since raw content includes the name"
1145 );
1146 }
1147
1148 #[test]
1149 fn test_dart_top_level_getter_setter() {
1150 let code = r#"
1151int _value = 0;
1152
1153int get currentValue {
1154 return _value;
1155}
1156
1157set currentValue(int v) {
1158 _value = v;
1159}
1160"#;
1161 let plugin = CodeParserPlugin;
1162 let entities = plugin.extract_entities(code, "accessors.dart");
1163 eprintln!(
1164 "Dart top-level accessors: {:?}",
1165 entities
1166 .iter()
1167 .map(|e| (&e.name, &e.entity_type, &e.content))
1168 .collect::<Vec<_>>()
1169 );
1170
1171 let getter = entities.iter().find(|e| e.name == "currentValue" && e.entity_type == "getter");
1172 assert!(getter.is_some(), "Should find top-level getter, got: {:?}",
1173 entities.iter().map(|e| (&e.name, &e.entity_type)).collect::<Vec<_>>());
1174 assert!(
1175 getter.unwrap().content.contains("return _value"),
1176 "Top-level getter content should include the body"
1177 );
1178 assert!(getter.unwrap().parent_id.is_none(), "Top-level getter should have no parent");
1179
1180 let setter = entities.iter().find(|e| e.name == "currentValue" && e.entity_type == "function");
1184 assert!(setter.is_some(), "Should find top-level setter as function, got: {:?}",
1185 entities.iter().map(|e| (&e.name, &e.entity_type)).collect::<Vec<_>>());
1186 assert!(
1187 setter.unwrap().content.contains("_value = v"),
1188 "Top-level setter content should include the body"
1189 );
1190 }
1191
1192 #[test]
1193 fn test_dart_field_entity_type() {
1194 let code = r#"
1195class Config {
1196 final String name;
1197 static const int maxRetries = 3;
1198}
1199"#;
1200 let plugin = CodeParserPlugin;
1201 let entities = plugin.extract_entities(code, "config.dart");
1202 eprintln!(
1203 "Dart fields: {:?}",
1204 entities
1205 .iter()
1206 .map(|e| (&e.name, &e.entity_type, &e.parent_id))
1207 .collect::<Vec<_>>()
1208 );
1209
1210 let name_field = entities.iter().find(|e| e.name == "name" && e.parent_id.is_some());
1211 assert!(name_field.is_some(), "Should find field 'name', got: {:?}",
1212 entities.iter().map(|e| (&e.name, &e.entity_type)).collect::<Vec<_>>());
1213 assert_eq!(name_field.unwrap().entity_type, "field");
1214
1215 let max_retries = entities.iter().find(|e| e.name == "maxRetries");
1216 assert!(max_retries.is_some(), "Should find field 'maxRetries', got: {:?}",
1217 entities.iter().map(|e| (&e.name, &e.entity_type)).collect::<Vec<_>>());
1218 assert_eq!(max_retries.unwrap().entity_type, "field");
1219 }
1220
1221 #[test]
1222 fn test_dart_identifier_list_fields() {
1223 let code = r#"
1227abstract class Shape {
1228 abstract double x, y;
1229 abstract String label;
1230}
1231"#;
1232 let plugin = CodeParserPlugin;
1233 let entities = plugin.extract_entities(code, "shape.dart");
1234 eprintln!(
1235 "Dart identifier_list fields: {:?}",
1236 entities
1237 .iter()
1238 .map(|e| (&e.name, &e.entity_type, &e.parent_id))
1239 .collect::<Vec<_>>()
1240 );
1241
1242 let x_field = entities.iter().find(|e| e.name == "x");
1243 assert!(x_field.is_some(), "Should find field 'x' from identifier_list, got: {:?}",
1244 entities.iter().map(|e| (&e.name, &e.entity_type)).collect::<Vec<_>>());
1245 assert_eq!(x_field.unwrap().entity_type, "field");
1246 assert!(x_field.unwrap().parent_id.is_some(), "field 'x' should be nested under Shape");
1247
1248 let label_field = entities.iter().find(|e| e.name == "label");
1249 assert!(label_field.is_some(), "Should find field 'label' from single-element identifier_list, got: {:?}",
1250 entities.iter().map(|e| (&e.name, &e.entity_type)).collect::<Vec<_>>());
1251 assert_eq!(label_field.unwrap().entity_type, "field");
1252 }
1253
1254 #[test]
1255 fn test_ocaml_entity_extraction() {
1256 let code = r#"
1257type color = Red | Green | Blue
1258
1259type point = {
1260 x : float;
1261 y : float;
1262}
1263
1264exception Not_found of string
1265
1266let greet name =
1267 Printf.printf "Hello, %s!\n" name
1268
1269let add a b = a + b
1270
1271let version = "1.0"
1272
1273let color_to_string = function
1274 | Red -> "red"
1275 | Blue -> "blue"
1276
1277let inc = fun x -> x + 1
1278
1279module MyModule = struct
1280 let helper x = x * 2
1281end
1282
1283module type Printable = sig
1284 val to_string : 'a -> string
1285end
1286
1287external caml_input : in_channel -> bytes -> int -> int -> int = "caml_input"
1288
1289class point_class x_init = object
1290 val mutable x = x_init
1291 method get_x = x
1292end
1293
1294class type measurable = object
1295 method measure : float
1296end
1297"#;
1298 let plugin = CodeParserPlugin;
1299 let entities = plugin.extract_entities(code, "example.ml");
1300 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
1301 eprintln!("OCaml entities: {:?}", entities.iter().map(|e| (&e.name, &e.entity_type)).collect::<Vec<_>>());
1302
1303 let find = |name: &str| entities.iter().find(|e| e.name == name)
1304 .unwrap_or_else(|| panic!("Should find {}, got: {:?}", name, names));
1305
1306 assert_eq!(find("color").entity_type, "type");
1307 assert_eq!(find("point").entity_type, "type");
1308 assert_eq!(find("Not_found").entity_type, "exception");
1309 assert_eq!(find("greet").entity_type, "function");
1310 assert_eq!(find("add").entity_type, "function");
1311 assert_eq!(find("version").entity_type, "value");
1312 assert_eq!(find("color_to_string").entity_type, "function");
1313 assert_eq!(find("inc").entity_type, "function");
1314 assert_eq!(find("MyModule").entity_type, "module");
1315 assert_eq!(find("Printable").entity_type, "module_type");
1316 assert_eq!(find("caml_input").entity_type, "external");
1317 assert_eq!(find("point_class").entity_type, "class");
1318 assert_eq!(find("measurable").entity_type, "class_type");
1319 }
1320
1321 #[test]
1322 fn test_ocaml_nested_module_entities() {
1323 let code = r#"
1324module Outer = struct
1325 let x = 42
1326
1327 module Inner = struct
1328 let y = 0
1329 end
1330end
1331"#;
1332 let plugin = CodeParserPlugin;
1333 let entities = plugin.extract_entities(code, "nested.ml");
1334 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
1335 eprintln!("OCaml nested: {:?}", entities.iter().map(|e| (&e.name, &e.entity_type, &e.parent_id)).collect::<Vec<_>>());
1336
1337 let find = |name: &str| entities.iter().find(|e| e.name == name)
1338 .unwrap_or_else(|| panic!("Should find {}, got: {:?}", name, names));
1339
1340 let outer = find("Outer");
1341 let x = find("x");
1342 let inner = find("Inner");
1343 let y = find("y");
1344
1345 assert_eq!(outer.entity_type, "module");
1346 assert_eq!(x.entity_type, "value");
1347 assert_eq!(inner.entity_type, "module");
1348 assert_eq!(y.entity_type, "value");
1349
1350 assert!(x.parent_id.as_ref().is_some_and(|p| p == &outer.id), "x should be nested under Outer");
1351 assert!(inner.parent_id.as_ref().is_some_and(|p| p == &outer.id), "Inner should be nested under Outer");
1352 assert!(y.parent_id.as_ref().is_some_and(|p| p == &inner.id), "y should be nested under Inner");
1353 }
1354
1355 #[test]
1356 fn test_ocaml_interface_entity_extraction() {
1357 let code = r#"
1358type t
1359
1360val create : string -> t
1361val to_string : t -> string
1362
1363exception Invalid_input of string
1364
1365module type Serializable = sig
1366 val serialize : t -> string
1367end
1368"#;
1369 let plugin = CodeParserPlugin;
1370 let entities = plugin.extract_entities(code, "example.mli");
1371 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
1372 eprintln!("OCaml interface entities: {:?}", entities.iter().map(|e| (&e.name, &e.entity_type)).collect::<Vec<_>>());
1373
1374 let find = |name: &str| entities.iter().find(|e| e.name == name)
1375 .unwrap_or_else(|| panic!("Should find {}, got: {:?}", name, names));
1376
1377 assert_eq!(find("t").entity_type, "type");
1378 assert_eq!(find("create").entity_type, "val");
1379 assert_eq!(find("to_string").entity_type, "val");
1380 assert_eq!(find("Invalid_input").entity_type, "exception");
1381 assert_eq!(find("Serializable").entity_type, "module_type");
1382 }
1383
1384 #[test]
1385 fn test_ocaml_mutual_recursion_let() {
1386 let code = r#"
1387let rec even n = (n = 0) || odd (n - 1)
1388and odd n = (n <> 0) && even (n - 1)
1389
1390let rec ping x = pong (x - 1)
1391and pong x = if x <= 0 then 0 else ping (x - 1)
1392"#;
1393 let plugin = CodeParserPlugin;
1394 let entities = plugin.extract_entities(code, "mutual.ml");
1395 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
1396 eprintln!("OCaml mutual let: {:?}", entities.iter().map(|e| (&e.name, &e.entity_type)).collect::<Vec<_>>());
1397
1398 let find = |name: &str| entities.iter().find(|e| e.name == name)
1399 .unwrap_or_else(|| panic!("Should find {}, got: {:?}", name, names));
1400
1401 assert_eq!(find("even").entity_type, "function");
1402 assert_eq!(find("odd").entity_type, "function");
1403 assert_eq!(find("ping").entity_type, "function");
1404 assert_eq!(find("pong").entity_type, "function");
1405 }
1406
1407 #[test]
1408 fn test_ocaml_mutual_recursion_module() {
1409 let code = r#"
1410module rec A : sig val x : int end = struct
1411 let x = B.y + 1
1412end
1413and B : sig val y : int end = struct
1414 let y = 0
1415end
1416"#;
1417 let plugin = CodeParserPlugin;
1418 let entities = plugin.extract_entities(code, "mutual_mod.ml");
1419 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
1420 eprintln!("OCaml mutual module: {:?}", entities.iter().map(|e| (&e.name, &e.entity_type, &e.parent_id)).collect::<Vec<_>>());
1421
1422 let find = |name: &str| entities.iter().find(|e| e.name == name)
1423 .unwrap_or_else(|| panic!("Should find {}, got: {:?}", name, names));
1424
1425 let a = find("A");
1426 let b = find("B");
1427 assert_eq!(a.entity_type, "module");
1428 assert_eq!(b.entity_type, "module");
1429
1430 let x = find("x");
1431 let y = find("y");
1432 assert!(x.parent_id.as_ref().is_some_and(|p| p == &a.id), "x should be nested under A");
1433 assert!(y.parent_id.as_ref().is_some_and(|p| p == &b.id), "y should be nested under B");
1434 }
1435
1436 #[test]
1437 fn test_ocaml_destructured_let() {
1438 let code = r#"
1439let (a, b) = (1, 2)
1440
1441let { x; y } = point
1442
1443let simple = 42
1444"#;
1445 let plugin = CodeParserPlugin;
1446 let entities = plugin.extract_entities(code, "destruct.ml");
1447 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
1448 eprintln!("OCaml destructured: {:?}", entities.iter().map(|e| (&e.name, &e.entity_type)).collect::<Vec<_>>());
1449
1450 let find = |name: &str| entities.iter().find(|e| e.name == name)
1451 .unwrap_or_else(|| panic!("Should find {}, got: {:?}", name, names));
1452
1453 assert_eq!(find("a").entity_type, "value");
1454 assert_eq!(find("b").entity_type, "value");
1455 assert_eq!(find("x").entity_type, "value");
1456 assert_eq!(find("y").entity_type, "value");
1457 assert_eq!(find("simple").entity_type, "value");
1458 }
1459
1460 #[test]
1461 fn test_ocaml_mutual_recursion_class() {
1462 let code = r#"
1463class foo = object
1464 method x = 1
1465end
1466and bar = object
1467 method y = 2
1468end
1469"#;
1470 let plugin = CodeParserPlugin;
1471 let entities = plugin.extract_entities(code, "classes.ml");
1472 let names: Vec<&str> = entities.iter().map(|e| e.name.as_str()).collect();
1473 eprintln!("OCaml mutual class: {:?}", entities.iter().map(|e| (&e.name, &e.entity_type)).collect::<Vec<_>>());
1474
1475 let find = |name: &str| entities.iter().find(|e| e.name == name)
1476 .unwrap_or_else(|| panic!("Should find {}, got: {:?}", name, names));
1477
1478 assert_eq!(find("foo").entity_type, "class");
1479 assert_eq!(find("bar").entity_type, "class");
1480 }
1481}