Skip to main content

sem_core/parser/plugins/code/
mod.rs

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
14// Thread-local parser cache: one Parser per language per thread.
15// Avoids creating a new Parser for every file during parallel graph builds.
16thread_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        // Methods should have Calculator as parent
136        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        // Verify nesting: create_user should have MyApp.Accounts as parent
315        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        // Existing language should still work
348        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        // Go doesn't have named nested functions, but has nested type/var declarations
486        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        // Structural hash should match since only the name differs
509        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        // Content hash should differ (it includes the name)
515        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        // Arrow function callbacks: locals are suppressed, but inner
637        // class/function declarations are still extracted. Nested callbacks
638        // also suppress their locals.
639        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        // Top-level entities preserved
676        assert!(top_level.contains(&"activeQueues"), "got: {:?}", top_level);
677        assert!(top_level.contains(&"handleFailure"), "got: {:?}", top_level);
678
679        // Declarations inside callback extracted
680        assert!(names.contains(&"QueueHandler"), "got: {:?}", names);
681        assert!(names.contains(&"handle"), "got: {:?}", names);
682        assert!(names.contains(&"createHandler"), "got: {:?}", names);
683
684        // Locals inside callbacks suppressed
685        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        // Function expressions: assigned to variables, or used as callback
783        // arguments. Locals are suppressed in all cases.
784        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        // Top-level variable declarations preserved
812        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        // Locals inside function expressions suppressed
817        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        // Named function expression used as callback argument not extracted
822        assert!(!top_level.contains(&"process"), "got: {:?}", top_level);
823    }
824
825    #[test]
826    fn test_variable_assigned_arrow_extracts_inner_entities() {
827        // Arrow function assigned to a variable: inner class/function
828        // declarations should be extracted, locals should be suppressed.
829        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        // Function expression assigned to a variable: same behavior.
856        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}