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}