Skip to main content

lean_ctx/core/
deep_queries.rs

1//! Tree-sitter deep queries for extracting imports, call sites, and type definitions.
2//!
3//! Replaces regex-based extraction in `deps.rs` with precise AST parsing.
4//! Supports: TypeScript/JavaScript, Python, Rust, Go, Java.
5
6#[cfg(feature = "tree-sitter")]
7use tree_sitter::{Language, Node, Parser};
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct ImportInfo {
11    pub source: String,
12    pub names: Vec<String>,
13    pub kind: ImportKind,
14    pub line: usize,
15    pub is_type_only: bool,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub enum ImportKind {
20    Named,
21    Default,
22    Star,
23    SideEffect,
24    Dynamic,
25    Reexport,
26}
27
28#[derive(Debug, Clone)]
29pub struct CallSite {
30    pub callee: String,
31    pub line: usize,
32    pub col: usize,
33    pub receiver: Option<String>,
34    pub is_method: bool,
35}
36
37#[derive(Debug, Clone)]
38pub struct TypeDef {
39    pub name: String,
40    pub kind: TypeDefKind,
41    pub line: usize,
42    pub end_line: usize,
43    pub is_exported: bool,
44    pub generics: Vec<String>,
45}
46
47#[derive(Debug, Clone, PartialEq)]
48pub enum TypeDefKind {
49    Class,
50    Interface,
51    TypeAlias,
52    Enum,
53    Struct,
54    Trait,
55    Protocol,
56    Record,
57    Annotation,
58    Union,
59}
60
61#[derive(Debug, Clone)]
62pub struct DeepAnalysis {
63    pub imports: Vec<ImportInfo>,
64    pub calls: Vec<CallSite>,
65    pub types: Vec<TypeDef>,
66    pub exports: Vec<String>,
67}
68
69impl DeepAnalysis {
70    pub fn empty() -> Self {
71        Self {
72            imports: Vec::new(),
73            calls: Vec::new(),
74            types: Vec::new(),
75            exports: Vec::new(),
76        }
77    }
78}
79
80pub fn analyze(content: &str, ext: &str) -> DeepAnalysis {
81    #[cfg(feature = "tree-sitter")]
82    {
83        if let Some(result) = analyze_with_tree_sitter(content, ext) {
84            return result;
85        }
86    }
87
88    let _ = (content, ext);
89    DeepAnalysis::empty()
90}
91
92#[cfg(feature = "tree-sitter")]
93fn analyze_with_tree_sitter(content: &str, ext: &str) -> Option<DeepAnalysis> {
94    let language = get_language(ext)?;
95    let mut parser = Parser::new();
96    parser.set_language(&language).ok()?;
97    let tree = parser.parse(content.as_bytes(), None)?;
98    let root = tree.root_node();
99
100    let imports = extract_imports(root, content, ext);
101    let calls = extract_calls(root, content, ext);
102    let types = extract_types(root, content, ext);
103    let exports = extract_exports(root, content, ext);
104
105    Some(DeepAnalysis {
106        imports,
107        calls,
108        types,
109        exports,
110    })
111}
112
113#[cfg(feature = "tree-sitter")]
114fn get_language(ext: &str) -> Option<Language> {
115    match ext {
116        "rs" => Some(tree_sitter_rust::LANGUAGE.into()),
117        "ts" | "tsx" => Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
118        "js" | "jsx" => Some(tree_sitter_javascript::LANGUAGE.into()),
119        "py" => Some(tree_sitter_python::LANGUAGE.into()),
120        "go" => Some(tree_sitter_go::LANGUAGE.into()),
121        "java" => Some(tree_sitter_java::LANGUAGE.into()),
122        "c" | "h" => Some(tree_sitter_c::LANGUAGE.into()),
123        "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => Some(tree_sitter_cpp::LANGUAGE.into()),
124        "rb" => Some(tree_sitter_ruby::LANGUAGE.into()),
125        "cs" => Some(tree_sitter_c_sharp::LANGUAGE.into()),
126        "kt" | "kts" => Some(tree_sitter_kotlin_ng::LANGUAGE.into()),
127        "swift" => Some(tree_sitter_swift::LANGUAGE.into()),
128        "php" => Some(tree_sitter_php::LANGUAGE_PHP.into()),
129        "sh" | "bash" => Some(tree_sitter_bash::LANGUAGE.into()),
130        "dart" => Some(tree_sitter_dart::LANGUAGE.into()),
131        "scala" | "sc" => Some(tree_sitter_scala::LANGUAGE.into()),
132        "ex" | "exs" => Some(tree_sitter_elixir::LANGUAGE.into()),
133        "zig" => Some(tree_sitter_zig::LANGUAGE.into()),
134        _ => None,
135    }
136}
137
138// ---------------------------------------------------------------------------
139// Imports
140// ---------------------------------------------------------------------------
141
142#[cfg(feature = "tree-sitter")]
143fn extract_imports(root: Node, src: &str, ext: &str) -> Vec<ImportInfo> {
144    match ext {
145        "ts" | "tsx" | "js" | "jsx" => extract_imports_ts(root, src),
146        "rs" => extract_imports_rust(root, src),
147        "py" => extract_imports_python(root, src),
148        "go" => extract_imports_go(root, src),
149        "java" => extract_imports_java(root, src),
150        "c" | "h" | "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => {
151            extract_imports_c_like(root, src)
152        }
153        "rb" => extract_imports_ruby(root, src),
154        "cs" => extract_imports_csharp(root, src),
155        "kt" | "kts" => extract_imports_kotlin(root, src),
156        "swift" => extract_imports_swift(root, src),
157        "php" => extract_imports_php(root, src),
158        "sh" | "bash" => extract_imports_bash(root, src),
159        "dart" => extract_imports_dart(root, src),
160        "scala" | "sc" => extract_imports_scala(root, src),
161        "ex" | "exs" => extract_imports_elixir(root, src),
162        "zig" => extract_imports_zig(root, src),
163        _ => Vec::new(),
164    }
165}
166
167#[cfg(feature = "tree-sitter")]
168fn extract_imports_c_like(root: Node, src: &str) -> Vec<ImportInfo> {
169    let mut imports = Vec::new();
170    let mut cursor = root.walk();
171
172    for node in root.children(&mut cursor) {
173        if node.kind() == "preproc_include" {
174            if let Some(s) = find_descendant_by_kind(node, "string_literal")
175                .or_else(|| find_descendant_by_kind(node, "system_lib_string"))
176            {
177                let raw = node_text(s, src);
178                let cleaned = raw
179                    .trim()
180                    .trim_start_matches('"')
181                    .trim_end_matches('"')
182                    .trim_start_matches('<')
183                    .trim_end_matches('>')
184                    .to_string();
185                if !cleaned.is_empty() {
186                    imports.push(ImportInfo {
187                        source: cleaned,
188                        names: Vec::new(),
189                        kind: ImportKind::Named,
190                        line: node.start_position().row + 1,
191                        is_type_only: false,
192                    });
193                }
194            }
195        }
196    }
197    imports
198}
199
200#[cfg(feature = "tree-sitter")]
201fn extract_imports_ruby(root: Node, src: &str) -> Vec<ImportInfo> {
202    let mut imports = Vec::new();
203    let mut cursor = root.walk();
204    for node in root.children(&mut cursor) {
205        let text = node_text(node, src).trim_start().to_string();
206        if text.starts_with("require ") || text.starts_with("require_relative ") {
207            if let Some(s) = find_descendant_by_kind(node, "string") {
208                let source_text = unquote(node_text(s, src));
209                if !source_text.is_empty() {
210                    imports.push(ImportInfo {
211                        source: source_text,
212                        names: Vec::new(),
213                        kind: ImportKind::Named,
214                        line: node.start_position().row + 1,
215                        is_type_only: false,
216                    });
217                }
218            }
219        }
220    }
221    imports
222}
223
224#[cfg(feature = "tree-sitter")]
225fn extract_imports_csharp(root: Node, src: &str) -> Vec<ImportInfo> {
226    let mut imports = Vec::new();
227    let mut cursor = root.walk();
228    for node in root.children(&mut cursor) {
229        if node.kind() == "using_directive" {
230            let text = node_text(node, src)
231                .trim()
232                .trim_start_matches("using")
233                .trim()
234                .trim_end_matches(';')
235                .trim()
236                .to_string();
237            if !text.is_empty() {
238                imports.push(ImportInfo {
239                    source: text,
240                    names: Vec::new(),
241                    kind: ImportKind::Named,
242                    line: node.start_position().row + 1,
243                    is_type_only: false,
244                });
245            }
246        }
247    }
248    imports
249}
250
251#[cfg(feature = "tree-sitter")]
252fn extract_imports_kotlin(root: Node, src: &str) -> Vec<ImportInfo> {
253    let mut imports = Vec::new();
254    let mut cursor = root.walk();
255    for node in root.children(&mut cursor) {
256        if node.kind() != "import" {
257            continue;
258        }
259        let Some(path_node) = find_child_by_kind(node, "qualified_identifier") else {
260            continue;
261        };
262        let source = node_text(path_node, src).to_string();
263        let text = node_text(node, src);
264
265        let path_end = path_node.end_byte();
266        let alias = {
267            let mut walk = node.walk();
268            let children: Vec<_> = node.children(&mut walk).collect();
269            children
270                .into_iter()
271                .find(|child| child.kind() == "identifier" && child.start_byte() > path_end)
272                .map(|child| node_text(child, src).to_string())
273        };
274        let is_star = text.contains(".*");
275
276        let names = if is_star {
277            vec!["*".to_string()]
278        } else if let Some(ref alias) = alias {
279            vec![alias.clone()]
280        } else {
281            vec![source.rsplit('.').next().unwrap_or(&source).to_string()]
282        };
283
284        imports.push(ImportInfo {
285            source,
286            names,
287            kind: if is_star {
288                ImportKind::Star
289            } else {
290                ImportKind::Named
291            },
292            line: node.start_position().row + 1,
293            is_type_only: false,
294        });
295    }
296    imports
297}
298
299#[cfg(feature = "tree-sitter")]
300fn extract_imports_swift(root: Node, src: &str) -> Vec<ImportInfo> {
301    let mut imports = Vec::new();
302    let mut cursor = root.walk();
303    for node in root.children(&mut cursor) {
304        if node.kind() == "import_declaration" {
305            let text = node_text(node, src)
306                .trim()
307                .trim_start_matches("import")
308                .trim()
309                .to_string();
310            if !text.is_empty() {
311                imports.push(ImportInfo {
312                    source: text,
313                    names: Vec::new(),
314                    kind: ImportKind::Named,
315                    line: node.start_position().row + 1,
316                    is_type_only: false,
317                });
318            }
319        }
320    }
321    imports
322}
323
324#[cfg(feature = "tree-sitter")]
325fn extract_imports_php(root: Node, src: &str) -> Vec<ImportInfo> {
326    let mut imports = Vec::new();
327    let mut cursor = root.walk();
328    for node in root.children(&mut cursor) {
329        let kind = node.kind();
330        if kind.contains("include") || kind.contains("require") {
331            if let Some(s) = find_descendant_by_kind(node, "string") {
332                let source_text = unquote(node_text(s, src));
333                if !source_text.is_empty() {
334                    imports.push(ImportInfo {
335                        source: source_text,
336                        names: Vec::new(),
337                        kind: ImportKind::Named,
338                        line: node.start_position().row + 1,
339                        is_type_only: false,
340                    });
341                }
342            }
343        }
344    }
345    imports
346}
347
348#[cfg(feature = "tree-sitter")]
349fn extract_imports_bash(root: Node, src: &str) -> Vec<ImportInfo> {
350    let mut imports = Vec::new();
351    let mut cursor = root.walk();
352    for node in root.children(&mut cursor) {
353        if node.kind() == "command" {
354            let text = node_text(node, src).trim().to_string();
355            if text.starts_with("source ") || text.starts_with(". ") {
356                let parts: Vec<&str> = text.split_whitespace().collect();
357                if parts.len() >= 2 {
358                    let p = parts[1].trim_matches('"').trim_matches('\'').to_string();
359                    if !p.is_empty() {
360                        imports.push(ImportInfo {
361                            source: p,
362                            names: Vec::new(),
363                            kind: ImportKind::Named,
364                            line: node.start_position().row + 1,
365                            is_type_only: false,
366                        });
367                    }
368                }
369            }
370        }
371    }
372    imports
373}
374
375#[cfg(feature = "tree-sitter")]
376fn extract_imports_dart(root: Node, src: &str) -> Vec<ImportInfo> {
377    let mut imports = Vec::new();
378    let mut cursor = root.walk();
379    for node in root.children(&mut cursor) {
380        if node.kind() == "import_or_export" || node.kind() == "library_import" {
381            if let Some(s) = find_descendant_by_kind(node, "string_literal")
382                .or_else(|| find_descendant_by_kind(node, "string"))
383            {
384                let source_text = unquote(node_text(s, src));
385                if !source_text.is_empty() {
386                    imports.push(ImportInfo {
387                        source: source_text,
388                        names: Vec::new(),
389                        kind: ImportKind::Named,
390                        line: node.start_position().row + 1,
391                        is_type_only: false,
392                    });
393                }
394            }
395        }
396    }
397    imports
398}
399
400#[cfg(feature = "tree-sitter")]
401fn extract_imports_scala(root: Node, src: &str) -> Vec<ImportInfo> {
402    let mut imports = Vec::new();
403    let mut cursor = root.walk();
404    for node in root.children(&mut cursor) {
405        if node.kind() == "import_declaration" {
406            let text = node_text(node, src)
407                .trim()
408                .trim_start_matches("import")
409                .trim()
410                .to_string();
411            if !text.is_empty() {
412                imports.push(ImportInfo {
413                    source: text,
414                    names: Vec::new(),
415                    kind: ImportKind::Named,
416                    line: node.start_position().row + 1,
417                    is_type_only: false,
418                });
419            }
420        }
421    }
422    imports
423}
424
425#[cfg(feature = "tree-sitter")]
426fn extract_imports_elixir(root: Node, src: &str) -> Vec<ImportInfo> {
427    let mut imports = Vec::new();
428    let mut cursor = root.walk();
429    for node in root.children(&mut cursor) {
430        let text = node_text(node, src).trim().to_string();
431        for kw in ["alias ", "import ", "require ", "use "] {
432            if text.starts_with(kw) {
433                let rest = text.trim_start_matches(kw).trim();
434                if !rest.is_empty() {
435                    let module = rest
436                        .split_whitespace()
437                        .next()
438                        .unwrap_or("")
439                        .trim_end_matches(',')
440                        .trim_end_matches(';')
441                        .to_string();
442                    if !module.is_empty() {
443                        imports.push(ImportInfo {
444                            source: module,
445                            names: Vec::new(),
446                            kind: ImportKind::Named,
447                            line: node.start_position().row + 1,
448                            is_type_only: false,
449                        });
450                    }
451                }
452            }
453        }
454    }
455    imports
456}
457
458#[cfg(feature = "tree-sitter")]
459fn extract_imports_zig(root: Node, src: &str) -> Vec<ImportInfo> {
460    let mut imports = Vec::new();
461    let mut cursor = root.walk();
462    for node in root.children(&mut cursor) {
463        let text = node_text(node, src);
464        if text.contains("@import") {
465            if let Some(s) = find_descendant_by_kind(node, "string_literal")
466                .or_else(|| find_descendant_by_kind(node, "string"))
467            {
468                let source_text = unquote(node_text(s, src));
469                if !source_text.is_empty() {
470                    imports.push(ImportInfo {
471                        source: source_text,
472                        names: Vec::new(),
473                        kind: ImportKind::Named,
474                        line: node.start_position().row + 1,
475                        is_type_only: false,
476                    });
477                }
478            }
479        }
480    }
481    imports
482}
483
484#[cfg(feature = "tree-sitter")]
485fn extract_imports_ts(root: Node, src: &str) -> Vec<ImportInfo> {
486    let mut imports = Vec::new();
487    let mut cursor = root.walk();
488
489    for node in root.children(&mut cursor) {
490        match node.kind() {
491            "import_statement" => {
492                if let Some(info) = parse_ts_import(node, src) {
493                    imports.push(info);
494                }
495            }
496            "export_statement" => {
497                if let Some(source) = find_child_by_kind(node, "string") {
498                    let source_text = unquote(node_text(source, src));
499                    let names = collect_named_imports(node, src);
500                    imports.push(ImportInfo {
501                        source: source_text,
502                        names,
503                        kind: ImportKind::Reexport,
504                        line: node.start_position().row + 1,
505                        is_type_only: false,
506                    });
507                }
508            }
509            _ => {}
510        }
511    }
512
513    walk_for_dynamic_imports(root, src, &mut imports);
514
515    imports
516}
517
518#[cfg(feature = "tree-sitter")]
519fn parse_ts_import(node: Node, src: &str) -> Option<ImportInfo> {
520    let source_node =
521        find_child_by_kind(node, "string").or_else(|| find_descendant_by_kind(node, "string"))?;
522    let source = unquote(node_text(source_node, src));
523
524    let is_type_only = node_text(node, src).starts_with("import type");
525
526    let clause = find_child_by_kind(node, "import_clause");
527    let (kind, names) = match clause {
528        Some(c) => classify_ts_import_clause(c, src),
529        None => (ImportKind::SideEffect, Vec::new()),
530    };
531
532    Some(ImportInfo {
533        source,
534        names,
535        kind,
536        line: node.start_position().row + 1,
537        is_type_only,
538    })
539}
540
541#[cfg(feature = "tree-sitter")]
542fn classify_ts_import_clause(clause: Node, src: &str) -> (ImportKind, Vec<String>) {
543    let mut names = Vec::new();
544    let mut has_default = false;
545    let mut has_star = false;
546
547    let mut cursor = clause.walk();
548    for child in clause.children(&mut cursor) {
549        match child.kind() {
550            "identifier" => {
551                has_default = true;
552                names.push(node_text(child, src).to_string());
553            }
554            "namespace_import" => {
555                has_star = true;
556                if let Some(id) = find_child_by_kind(child, "identifier") {
557                    names.push(format!("* as {}", node_text(id, src)));
558                }
559            }
560            "named_imports" => {
561                let mut inner = child.walk();
562                for spec in child.children(&mut inner) {
563                    if spec.kind() == "import_specifier" {
564                        let name = find_child_by_kind(spec, "identifier")
565                            .map(|n| node_text(n, src).to_string());
566                        if let Some(n) = name {
567                            names.push(n);
568                        }
569                    }
570                }
571            }
572            _ => {}
573        }
574    }
575
576    let kind = if has_star {
577        ImportKind::Star
578    } else if has_default && names.len() == 1 {
579        ImportKind::Default
580    } else {
581        ImportKind::Named
582    };
583
584    (kind, names)
585}
586
587#[cfg(feature = "tree-sitter")]
588fn walk_for_dynamic_imports(node: Node, src: &str, imports: &mut Vec<ImportInfo>) {
589    if node.kind() == "call_expression" {
590        let callee = find_child_by_kind(node, "import");
591        if callee.is_some() {
592            if let Some(args) = find_child_by_kind(node, "arguments") {
593                if let Some(first_arg) = find_child_by_kind(args, "string") {
594                    imports.push(ImportInfo {
595                        source: unquote(node_text(first_arg, src)),
596                        names: Vec::new(),
597                        kind: ImportKind::Dynamic,
598                        line: node.start_position().row + 1,
599                        is_type_only: false,
600                    });
601                }
602            }
603        }
604    }
605    let mut cursor = node.walk();
606    for child in node.children(&mut cursor) {
607        walk_for_dynamic_imports(child, src, imports);
608    }
609}
610
611#[cfg(feature = "tree-sitter")]
612fn extract_imports_rust(root: Node, src: &str) -> Vec<ImportInfo> {
613    let mut imports = Vec::new();
614    let mut cursor = root.walk();
615
616    for node in root.children(&mut cursor) {
617        if node.kind() == "mod_item" {
618            let text = node_text(node, src);
619            if !text.contains('{') {
620                if let Some(name_node) = find_child_by_kind(node, "identifier") {
621                    let mod_name = node_text(name_node, src).to_string();
622                    imports.push(ImportInfo {
623                        source: mod_name.clone(),
624                        names: vec![mod_name],
625                        kind: ImportKind::Named,
626                        line: node.start_position().row + 1,
627                        is_type_only: false,
628                    });
629                }
630            }
631        } else if node.kind() == "use_declaration" {
632            let is_pub = node_text(node, src).trim_start().starts_with("pub");
633            let kind = if is_pub {
634                ImportKind::Reexport
635            } else {
636                ImportKind::Named
637            };
638
639            if let Some(arg) = find_child_by_kind(node, "use_as_clause")
640                .or_else(|| find_child_by_kind(node, "scoped_identifier"))
641                .or_else(|| find_child_by_kind(node, "scoped_use_list"))
642                .or_else(|| find_child_by_kind(node, "use_wildcard"))
643                .or_else(|| find_child_by_kind(node, "identifier"))
644            {
645                let full_path = node_text(arg, src).to_string();
646
647                let (source, names) = if full_path.contains('{') {
648                    let parts: Vec<&str> = full_path.splitn(2, "::").collect();
649                    let base = parts[0].to_string();
650                    let items: Vec<String> = full_path
651                        .split('{')
652                        .nth(1)
653                        .unwrap_or("")
654                        .trim_end_matches('}')
655                        .split(',')
656                        .map(|s| s.trim().to_string())
657                        .filter(|s| !s.is_empty())
658                        .collect();
659                    (base, items)
660                } else if full_path.ends_with("::*") {
661                    (
662                        full_path.trim_end_matches("::*").to_string(),
663                        vec!["*".to_string()],
664                    )
665                } else {
666                    let name = full_path.rsplit("::").next().unwrap_or(&full_path);
667                    (full_path.clone(), vec![name.to_string()])
668                };
669
670                let is_std = source.starts_with("std")
671                    || source.starts_with("core")
672                    || source.starts_with("alloc");
673                if !is_std {
674                    imports.push(ImportInfo {
675                        source,
676                        names,
677                        kind: if full_path.contains('*') {
678                            ImportKind::Star
679                        } else {
680                            kind.clone()
681                        },
682                        line: node.start_position().row + 1,
683                        is_type_only: false,
684                    });
685                }
686            }
687        }
688    }
689
690    imports
691}
692
693#[cfg(feature = "tree-sitter")]
694fn extract_imports_python(root: Node, src: &str) -> Vec<ImportInfo> {
695    let mut imports = Vec::new();
696    let mut cursor = root.walk();
697
698    for node in root.children(&mut cursor) {
699        match node.kind() {
700            "import_statement" => {
701                let mut inner = node.walk();
702                for child in node.children(&mut inner) {
703                    if child.kind() == "dotted_name" || child.kind() == "aliased_import" {
704                        let text = node_text(child, src);
705                        let module = if child.kind() == "aliased_import" {
706                            find_child_by_kind(child, "dotted_name")
707                                .map(|n| node_text(n, src).to_string())
708                                .unwrap_or_else(|| text.to_string())
709                        } else {
710                            text.to_string()
711                        };
712                        imports.push(ImportInfo {
713                            source: module,
714                            names: Vec::new(),
715                            kind: ImportKind::Named,
716                            line: node.start_position().row + 1,
717                            is_type_only: false,
718                        });
719                    }
720                }
721            }
722            "import_from_statement" => {
723                let module = find_child_by_kind(node, "dotted_name")
724                    .or_else(|| find_child_by_kind(node, "relative_import"))
725                    .map(|n| node_text(n, src).to_string())
726                    .unwrap_or_default();
727
728                let mut names = Vec::new();
729                let mut is_star = false;
730
731                let mut inner = node.walk();
732                for child in node.children(&mut inner) {
733                    if child.kind() == "wildcard_import" {
734                        is_star = true;
735                    } else if child.kind() == "import_prefix" {
736                        // relative import dots handled via module already
737                    } else if child.kind() == "dotted_name"
738                        && child.start_position() != node.start_position()
739                    {
740                        names.push(node_text(child, src).to_string());
741                    } else if child.kind() == "aliased_import" {
742                        if let Some(n) = find_child_by_kind(child, "dotted_name")
743                            .or_else(|| find_child_by_kind(child, "identifier"))
744                        {
745                            names.push(node_text(n, src).to_string());
746                        }
747                    }
748                }
749
750                imports.push(ImportInfo {
751                    source: module,
752                    names,
753                    kind: if is_star {
754                        ImportKind::Star
755                    } else {
756                        ImportKind::Named
757                    },
758                    line: node.start_position().row + 1,
759                    is_type_only: false,
760                });
761            }
762            _ => {}
763        }
764    }
765
766    imports
767}
768
769#[cfg(feature = "tree-sitter")]
770fn extract_imports_go(root: Node, src: &str) -> Vec<ImportInfo> {
771    let mut imports = Vec::new();
772    let mut cursor = root.walk();
773
774    for node in root.children(&mut cursor) {
775        if node.kind() == "import_declaration" {
776            let mut inner = node.walk();
777            for child in node.children(&mut inner) {
778                match child.kind() {
779                    "import_spec" => {
780                        if let Some(path_node) =
781                            find_child_by_kind(child, "interpreted_string_literal")
782                        {
783                            let source = unquote(node_text(path_node, src));
784                            let alias = find_child_by_kind(child, "package_identifier")
785                                .or_else(|| find_child_by_kind(child, "dot"))
786                                .or_else(|| find_child_by_kind(child, "blank_identifier"));
787                            let kind = match alias.map(|a| node_text(a, src)) {
788                                Some(".") => ImportKind::Star,
789                                Some("_") => ImportKind::SideEffect,
790                                _ => ImportKind::Named,
791                            };
792                            imports.push(ImportInfo {
793                                source,
794                                names: Vec::new(),
795                                kind,
796                                line: child.start_position().row + 1,
797                                is_type_only: false,
798                            });
799                        }
800                    }
801                    "import_spec_list" => {
802                        let mut spec_cursor = child.walk();
803                        for spec in child.children(&mut spec_cursor) {
804                            if spec.kind() == "import_spec" {
805                                if let Some(path_node) =
806                                    find_child_by_kind(spec, "interpreted_string_literal")
807                                {
808                                    let source = unquote(node_text(path_node, src));
809                                    let alias = find_child_by_kind(spec, "package_identifier")
810                                        .or_else(|| find_child_by_kind(spec, "dot"))
811                                        .or_else(|| find_child_by_kind(spec, "blank_identifier"));
812                                    let kind = match alias.map(|a| node_text(a, src)) {
813                                        Some(".") => ImportKind::Star,
814                                        Some("_") => ImportKind::SideEffect,
815                                        _ => ImportKind::Named,
816                                    };
817                                    imports.push(ImportInfo {
818                                        source,
819                                        names: Vec::new(),
820                                        kind,
821                                        line: spec.start_position().row + 1,
822                                        is_type_only: false,
823                                    });
824                                }
825                            }
826                        }
827                    }
828                    "interpreted_string_literal" => {
829                        let source = unquote(node_text(child, src));
830                        imports.push(ImportInfo {
831                            source,
832                            names: Vec::new(),
833                            kind: ImportKind::Named,
834                            line: child.start_position().row + 1,
835                            is_type_only: false,
836                        });
837                    }
838                    _ => {}
839                }
840            }
841        }
842    }
843
844    imports
845}
846
847#[cfg(feature = "tree-sitter")]
848fn extract_imports_java(root: Node, src: &str) -> Vec<ImportInfo> {
849    let mut imports = Vec::new();
850    let mut cursor = root.walk();
851
852    for node in root.children(&mut cursor) {
853        if node.kind() == "import_declaration" {
854            let text = node_text(node, src).to_string();
855            let _is_static = text.contains("static ");
856
857            let path_node = find_child_by_kind(node, "scoped_identifier")
858                .or_else(|| find_child_by_kind(node, "identifier"));
859            if let Some(p) = path_node {
860                let full_path = node_text(p, src).to_string();
861
862                let is_wildcard = find_child_by_kind(node, "asterisk").is_some();
863                let kind = if is_wildcard {
864                    ImportKind::Star
865                } else {
866                    ImportKind::Named
867                };
868
869                let name = full_path
870                    .rsplit('.')
871                    .next()
872                    .unwrap_or(&full_path)
873                    .to_string();
874                imports.push(ImportInfo {
875                    source: full_path,
876                    names: vec![name],
877                    kind,
878                    line: node.start_position().row + 1,
879                    is_type_only: false,
880                });
881            }
882        }
883    }
884
885    imports
886}
887
888// ---------------------------------------------------------------------------
889// Call Sites
890// ---------------------------------------------------------------------------
891
892#[cfg(feature = "tree-sitter")]
893fn extract_calls(root: Node, src: &str, ext: &str) -> Vec<CallSite> {
894    let mut calls = Vec::new();
895    walk_calls(root, src, ext, &mut calls);
896    calls
897}
898
899#[cfg(feature = "tree-sitter")]
900fn walk_calls(node: Node, src: &str, ext: &str, calls: &mut Vec<CallSite>) {
901    if node.kind() == "call_expression" || node.kind() == "method_invocation" {
902        if let Some(call) = parse_call(node, src, ext) {
903            calls.push(call);
904        }
905    }
906
907    let mut cursor = node.walk();
908    for child in node.children(&mut cursor) {
909        walk_calls(child, src, ext, calls);
910    }
911}
912
913#[cfg(feature = "tree-sitter")]
914fn parse_call(node: Node, src: &str, ext: &str) -> Option<CallSite> {
915    match ext {
916        "ts" | "tsx" | "js" | "jsx" => parse_call_ts(node, src),
917        "rs" => parse_call_rust(node, src),
918        "py" => parse_call_python(node, src),
919        "go" => parse_call_go(node, src),
920        "java" => parse_call_java(node, src),
921        "kt" | "kts" => parse_call_kotlin(node, src),
922        _ => None,
923    }
924}
925
926#[cfg(feature = "tree-sitter")]
927fn parse_call_ts(node: Node, src: &str) -> Option<CallSite> {
928    let func = find_child_by_kind(node, "member_expression")
929        .or_else(|| find_child_by_kind(node, "identifier"))
930        .or_else(|| find_child_by_kind(node, "subscript_expression"))?;
931
932    if func.kind() == "member_expression" {
933        let obj =
934            find_child_by_kind(func, "identifier").or_else(|| find_child_by_kind(func, "this"))?;
935        let prop = find_child_by_kind(func, "property_identifier")?;
936        Some(CallSite {
937            callee: node_text(prop, src).to_string(),
938            line: node.start_position().row + 1,
939            col: node.start_position().column,
940            receiver: Some(node_text(obj, src).to_string()),
941            is_method: true,
942        })
943    } else {
944        Some(CallSite {
945            callee: node_text(func, src).to_string(),
946            line: node.start_position().row + 1,
947            col: node.start_position().column,
948            receiver: None,
949            is_method: false,
950        })
951    }
952}
953
954#[cfg(feature = "tree-sitter")]
955fn parse_call_rust(node: Node, src: &str) -> Option<CallSite> {
956    let func = node.child(0)?;
957    match func.kind() {
958        "field_expression" => {
959            let field = find_child_by_kind(func, "field_identifier")?;
960            let receiver = func.child(0).map(|r| node_text(r, src).to_string());
961            Some(CallSite {
962                callee: node_text(field, src).to_string(),
963                line: node.start_position().row + 1,
964                col: node.start_position().column,
965                receiver,
966                is_method: true,
967            })
968        }
969        "scoped_identifier" | "identifier" => Some(CallSite {
970            callee: node_text(func, src).to_string(),
971            line: node.start_position().row + 1,
972            col: node.start_position().column,
973            receiver: None,
974            is_method: false,
975        }),
976        _ => None,
977    }
978}
979
980#[cfg(feature = "tree-sitter")]
981fn parse_call_python(node: Node, src: &str) -> Option<CallSite> {
982    let func = node.child(0)?;
983    match func.kind() {
984        "attribute" => {
985            let attr = find_child_by_kind(func, "identifier");
986            let obj = func.child(0).map(|r| node_text(r, src).to_string());
987            let name = attr
988                .map(|a| node_text(a, src).to_string())
989                .or_else(|| {
990                    let text = node_text(func, src);
991                    text.rsplit('.').next().map(|s| s.to_string())
992                })
993                .unwrap_or_default();
994            Some(CallSite {
995                callee: name,
996                line: node.start_position().row + 1,
997                col: node.start_position().column,
998                receiver: obj,
999                is_method: true,
1000            })
1001        }
1002        "identifier" => Some(CallSite {
1003            callee: node_text(func, src).to_string(),
1004            line: node.start_position().row + 1,
1005            col: node.start_position().column,
1006            receiver: None,
1007            is_method: false,
1008        }),
1009        _ => None,
1010    }
1011}
1012
1013#[cfg(feature = "tree-sitter")]
1014fn parse_call_go(node: Node, src: &str) -> Option<CallSite> {
1015    let func = node.child(0)?;
1016    match func.kind() {
1017        "selector_expression" => {
1018            let field = find_child_by_kind(func, "field_identifier")?;
1019            let obj = func.child(0).map(|r| node_text(r, src).to_string());
1020            Some(CallSite {
1021                callee: node_text(field, src).to_string(),
1022                line: node.start_position().row + 1,
1023                col: node.start_position().column,
1024                receiver: obj,
1025                is_method: true,
1026            })
1027        }
1028        "identifier" => Some(CallSite {
1029            callee: node_text(func, src).to_string(),
1030            line: node.start_position().row + 1,
1031            col: node.start_position().column,
1032            receiver: None,
1033            is_method: false,
1034        }),
1035        _ => None,
1036    }
1037}
1038
1039#[cfg(feature = "tree-sitter")]
1040fn parse_call_java(node: Node, src: &str) -> Option<CallSite> {
1041    if node.kind() == "method_invocation" {
1042        let name = find_child_by_kind(node, "identifier")?;
1043        let obj = find_child_by_kind(node, "field_access")
1044            .or_else(|| {
1045                let first = node.child(0)?;
1046                if first.kind() == "identifier" && first.id() != name.id() {
1047                    Some(first)
1048                } else {
1049                    None
1050                }
1051            })
1052            .map(|o| node_text(o, src).to_string());
1053        return Some(CallSite {
1054            callee: node_text(name, src).to_string(),
1055            line: node.start_position().row + 1,
1056            col: node.start_position().column,
1057            receiver: obj,
1058            is_method: true,
1059        });
1060    }
1061
1062    let func = node.child(0)?;
1063    Some(CallSite {
1064        callee: node_text(func, src).to_string(),
1065        line: node.start_position().row + 1,
1066        col: node.start_position().column,
1067        receiver: None,
1068        is_method: false,
1069    })
1070}
1071
1072#[cfg(feature = "tree-sitter")]
1073fn parse_call_kotlin(node: Node, src: &str) -> Option<CallSite> {
1074    let callee = node.child(0)?;
1075
1076    match callee.kind() {
1077        "identifier" => Some(CallSite {
1078            callee: node_text(callee, src).to_string(),
1079            line: node.start_position().row + 1,
1080            col: node.start_position().column,
1081            receiver: None,
1082            is_method: false,
1083        }),
1084        "navigation_expression" => {
1085            let mut cursor = callee.walk();
1086            let children: Vec<Node> = callee.children(&mut cursor).collect();
1087            let callee_name = children
1088                .iter()
1089                .rev()
1090                .find(|child| child.kind() == "identifier")
1091                .map(|child| node_text(*child, src).to_string())?;
1092            let receiver = children
1093                .iter()
1094                .find(|child| {
1095                    matches!(
1096                        child.kind(),
1097                        "expression"
1098                            | "primary_expression"
1099                            | "identifier"
1100                            | "navigation_expression"
1101                            | "this_expression"
1102                            | "super_expression"
1103                    )
1104                })
1105                .map(|child| node_text(*child, src).to_string())
1106                .filter(|text| text != &callee_name);
1107
1108            Some(CallSite {
1109                callee: callee_name,
1110                line: node.start_position().row + 1,
1111                col: node.start_position().column,
1112                receiver,
1113                is_method: true,
1114            })
1115        }
1116        _ => find_descendant_by_kind(callee, "identifier").map(|name| CallSite {
1117            callee: node_text(name, src).to_string(),
1118            line: node.start_position().row + 1,
1119            col: node.start_position().column,
1120            receiver: None,
1121            is_method: false,
1122        }),
1123    }
1124}
1125
1126// ---------------------------------------------------------------------------
1127// Type Definitions
1128// ---------------------------------------------------------------------------
1129
1130#[cfg(feature = "tree-sitter")]
1131fn extract_types(root: Node, src: &str, ext: &str) -> Vec<TypeDef> {
1132    let mut types = Vec::new();
1133    walk_types(root, src, ext, &mut types, false);
1134    types
1135}
1136
1137#[cfg(feature = "tree-sitter")]
1138fn walk_types(node: Node, src: &str, ext: &str, types: &mut Vec<TypeDef>, parent_exported: bool) {
1139    let exported = parent_exported || is_exported_node(node, src, ext);
1140
1141    if let Some(td) = match_type_def(node, src, ext, exported) {
1142        types.push(td);
1143    }
1144
1145    let mut cursor = node.walk();
1146    for child in node.children(&mut cursor) {
1147        walk_types(child, src, ext, types, exported);
1148    }
1149}
1150
1151#[cfg(feature = "tree-sitter")]
1152fn match_type_def(node: Node, src: &str, ext: &str, parent_exported: bool) -> Option<TypeDef> {
1153    let (name, kind) = match ext {
1154        "ts" | "tsx" | "js" | "jsx" => match_type_def_ts(node, src)?,
1155        "rs" => match_type_def_rust(node, src)?,
1156        "py" => match_type_def_python(node, src)?,
1157        "go" => match_type_def_go(node, src)?,
1158        "java" => match_type_def_java(node, src)?,
1159        "kt" | "kts" => match_type_def_kotlin(node, src)?,
1160        _ => return None,
1161    };
1162
1163    let is_exported = parent_exported || is_exported_node(node, src, ext);
1164    let generics = extract_generics(node, src);
1165
1166    Some(TypeDef {
1167        name,
1168        kind,
1169        line: node.start_position().row + 1,
1170        end_line: node.end_position().row + 1,
1171        is_exported,
1172        generics,
1173    })
1174}
1175
1176#[cfg(feature = "tree-sitter")]
1177fn match_type_def_ts(node: Node, src: &str) -> Option<(String, TypeDefKind)> {
1178    match node.kind() {
1179        "class_declaration" | "abstract_class_declaration" => {
1180            let name = find_child_by_kind(node, "type_identifier")
1181                .or_else(|| find_child_by_kind(node, "identifier"))?;
1182            Some((node_text(name, src).to_string(), TypeDefKind::Class))
1183        }
1184        "interface_declaration" => {
1185            let name = find_child_by_kind(node, "type_identifier")?;
1186            Some((node_text(name, src).to_string(), TypeDefKind::Interface))
1187        }
1188        "type_alias_declaration" => {
1189            let name = find_child_by_kind(node, "type_identifier")?;
1190            let text = node_text(node, src);
1191            let kind = if text.contains(" | ") {
1192                TypeDefKind::Union
1193            } else {
1194                TypeDefKind::TypeAlias
1195            };
1196            Some((node_text(name, src).to_string(), kind))
1197        }
1198        "enum_declaration" => {
1199            let name = find_child_by_kind(node, "identifier")?;
1200            Some((node_text(name, src).to_string(), TypeDefKind::Enum))
1201        }
1202        _ => None,
1203    }
1204}
1205
1206#[cfg(feature = "tree-sitter")]
1207fn match_type_def_rust(node: Node, src: &str) -> Option<(String, TypeDefKind)> {
1208    match node.kind() {
1209        "struct_item" => {
1210            let name = find_child_by_kind(node, "type_identifier")?;
1211            Some((node_text(name, src).to_string(), TypeDefKind::Struct))
1212        }
1213        "enum_item" => {
1214            let name = find_child_by_kind(node, "type_identifier")?;
1215            Some((node_text(name, src).to_string(), TypeDefKind::Enum))
1216        }
1217        "trait_item" => {
1218            let name = find_child_by_kind(node, "type_identifier")?;
1219            Some((node_text(name, src).to_string(), TypeDefKind::Trait))
1220        }
1221        "type_item" => {
1222            let name = find_child_by_kind(node, "type_identifier")?;
1223            Some((node_text(name, src).to_string(), TypeDefKind::TypeAlias))
1224        }
1225        _ => None,
1226    }
1227}
1228
1229#[cfg(feature = "tree-sitter")]
1230fn match_type_def_python(node: Node, src: &str) -> Option<(String, TypeDefKind)> {
1231    if node.kind() == "class_definition" {
1232        let name = find_child_by_kind(node, "identifier")?;
1233        let text = node_text(node, src);
1234        let kind = if text.contains("Protocol") {
1235            TypeDefKind::Protocol
1236        } else if text.contains("TypedDict") || text.contains("@dataclass") {
1237            TypeDefKind::Struct
1238        } else if text.contains("Enum") {
1239            TypeDefKind::Enum
1240        } else {
1241            TypeDefKind::Class
1242        };
1243        Some((node_text(name, src).to_string(), kind))
1244    } else {
1245        None
1246    }
1247}
1248
1249#[cfg(feature = "tree-sitter")]
1250fn match_type_def_go(node: Node, src: &str) -> Option<(String, TypeDefKind)> {
1251    if node.kind() == "type_spec" {
1252        let name = find_child_by_kind(node, "type_identifier")?;
1253        let count = node.child_count();
1254        let type_body = node.child((count.saturating_sub(1)) as u32)?;
1255        let kind = match type_body.kind() {
1256            "struct_type" => TypeDefKind::Struct,
1257            "interface_type" => TypeDefKind::Interface,
1258            _ => TypeDefKind::TypeAlias,
1259        };
1260        Some((node_text(name, src).to_string(), kind))
1261    } else {
1262        None
1263    }
1264}
1265
1266#[cfg(feature = "tree-sitter")]
1267fn match_type_def_java(node: Node, src: &str) -> Option<(String, TypeDefKind)> {
1268    match node.kind() {
1269        "class_declaration" => {
1270            let name = find_child_by_kind(node, "identifier")?;
1271            Some((node_text(name, src).to_string(), TypeDefKind::Class))
1272        }
1273        "interface_declaration" => {
1274            let name = find_child_by_kind(node, "identifier")?;
1275            Some((node_text(name, src).to_string(), TypeDefKind::Interface))
1276        }
1277        "enum_declaration" => {
1278            let name = find_child_by_kind(node, "identifier")?;
1279            Some((node_text(name, src).to_string(), TypeDefKind::Enum))
1280        }
1281        "record_declaration" => {
1282            let name = find_child_by_kind(node, "identifier")?;
1283            Some((node_text(name, src).to_string(), TypeDefKind::Record))
1284        }
1285        "annotation_type_declaration" => {
1286            let name = find_child_by_kind(node, "identifier")?;
1287            Some((node_text(name, src).to_string(), TypeDefKind::Annotation))
1288        }
1289        _ => None,
1290    }
1291}
1292
1293#[cfg(feature = "tree-sitter")]
1294fn match_type_def_kotlin(node: Node, src: &str) -> Option<(String, TypeDefKind)> {
1295    match node.kind() {
1296        "class_declaration" => {
1297            let name = node
1298                .child_by_field_name("name")
1299                .or_else(|| find_child_by_kind(node, "identifier"))?;
1300            let text = node_text(node, src);
1301            let kind = if text.contains("interface") {
1302                TypeDefKind::Interface
1303            } else if text.contains("enum class") {
1304                TypeDefKind::Enum
1305            } else {
1306                TypeDefKind::Class
1307            };
1308            Some((node_text(name, src).to_string(), kind))
1309        }
1310        "object_declaration" => {
1311            let name = node
1312                .child_by_field_name("name")
1313                .or_else(|| find_child_by_kind(node, "identifier"))?;
1314            Some((node_text(name, src).to_string(), TypeDefKind::Class))
1315        }
1316        "type_alias" => {
1317            let name = node
1318                .child_by_field_name("type")
1319                .or_else(|| find_child_by_kind(node, "identifier"))?;
1320            Some((node_text(name, src).to_string(), TypeDefKind::TypeAlias))
1321        }
1322        _ => None,
1323    }
1324}
1325
1326// ---------------------------------------------------------------------------
1327// Exports
1328// ---------------------------------------------------------------------------
1329
1330#[cfg(feature = "tree-sitter")]
1331fn extract_exports(root: Node, src: &str, ext: &str) -> Vec<String> {
1332    let mut exports = Vec::new();
1333    walk_exports(root, src, ext, &mut exports);
1334    exports
1335}
1336
1337#[cfg(feature = "tree-sitter")]
1338fn walk_exports(node: Node, src: &str, ext: &str, exports: &mut Vec<String>) {
1339    if is_exported_node(node, src, ext) {
1340        if let Some(name) = get_declaration_name(node, src) {
1341            exports.push(name);
1342        }
1343    }
1344    let mut cursor = node.walk();
1345    for child in node.children(&mut cursor) {
1346        walk_exports(child, src, ext, exports);
1347    }
1348}
1349
1350#[cfg(feature = "tree-sitter")]
1351fn is_exported_node(node: Node, src: &str, ext: &str) -> bool {
1352    match ext {
1353        "ts" | "tsx" | "js" | "jsx" => {
1354            node.kind() == "export_statement"
1355                || node
1356                    .parent()
1357                    .is_some_and(|p| p.kind() == "export_statement")
1358        }
1359        "rs" => node_text(node, src).trim_start().starts_with("pub "),
1360        "go" => {
1361            if let Some(name) = get_declaration_name(node, src) {
1362                name.starts_with(char::is_uppercase)
1363            } else {
1364                false
1365            }
1366        }
1367        "java" => node_text(node, src).trim_start().starts_with("public "),
1368        "kt" | "kts" => kotlin_declaration_exported(node, src),
1369        "py" => {
1370            if let Some(name) = get_declaration_name(node, src) {
1371                !name.starts_with('_')
1372            } else {
1373                false
1374            }
1375        }
1376        _ => false,
1377    }
1378}
1379
1380#[cfg(feature = "tree-sitter")]
1381fn get_declaration_name(node: Node, src: &str) -> Option<String> {
1382    for kind in &[
1383        "identifier",
1384        "type_identifier",
1385        "property_identifier",
1386        "field_identifier",
1387    ] {
1388        if let Some(name_node) = find_child_by_kind(node, kind) {
1389            return Some(node_text(name_node, src).to_string());
1390        }
1391    }
1392    None
1393}
1394
1395#[cfg(feature = "tree-sitter")]
1396fn kotlin_declaration_exported(node: Node, src: &str) -> bool {
1397    if let Some(modifiers) = find_child_by_kind(node, "modifiers") {
1398        !node_text(modifiers, src).contains("private")
1399    } else {
1400        !node_text(node, src).contains("private")
1401    }
1402}
1403
1404#[cfg(feature = "tree-sitter")]
1405fn extract_generics(node: Node, src: &str) -> Vec<String> {
1406    let tp = find_child_by_kind(node, "type_parameters")
1407        .or_else(|| find_child_by_kind(node, "type_parameter_list"));
1408    match tp {
1409        Some(params) => {
1410            let mut result = Vec::new();
1411            let mut cursor = params.walk();
1412            for child in params.children(&mut cursor) {
1413                if child.kind() == "type_parameter"
1414                    || child.kind() == "type_identifier"
1415                    || child.kind() == "identifier"
1416                {
1417                    result.push(node_text(child, src).to_string());
1418                }
1419            }
1420            result
1421        }
1422        None => Vec::new(),
1423    }
1424}
1425
1426// ---------------------------------------------------------------------------
1427// Helpers
1428// ---------------------------------------------------------------------------
1429
1430#[cfg(feature = "tree-sitter")]
1431fn node_text<'a>(node: Node, src: &'a str) -> &'a str {
1432    &src[node.byte_range()]
1433}
1434
1435#[cfg(feature = "tree-sitter")]
1436fn find_child_by_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1437    let mut cursor = node.walk();
1438    let result = node.children(&mut cursor).find(|c| c.kind() == kind);
1439    result
1440}
1441
1442#[cfg(feature = "tree-sitter")]
1443fn find_descendant_by_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1444    if node.kind() == kind {
1445        return Some(node);
1446    }
1447    let mut cursor = node.walk();
1448    for child in node.children(&mut cursor) {
1449        if let Some(found) = find_descendant_by_kind(child, kind) {
1450            return Some(found);
1451        }
1452    }
1453    None
1454}
1455
1456#[cfg(feature = "tree-sitter")]
1457fn collect_named_imports(node: Node, src: &str) -> Vec<String> {
1458    let mut names = Vec::new();
1459    if let Some(named) = find_descendant_by_kind(node, "named_imports") {
1460        let mut cursor = named.walk();
1461        for child in named.children(&mut cursor) {
1462            if child.kind() == "import_specifier" || child.kind() == "export_specifier" {
1463                if let Some(id) = find_child_by_kind(child, "identifier") {
1464                    names.push(node_text(id, src).to_string());
1465                }
1466            }
1467        }
1468    }
1469    names
1470}
1471
1472fn unquote(s: &str) -> String {
1473    s.trim_matches(|c| c == '\'' || c == '"' || c == '`')
1474        .to_string()
1475}
1476
1477// ---------------------------------------------------------------------------
1478// Tests
1479// ---------------------------------------------------------------------------
1480
1481#[cfg(test)]
1482#[cfg(feature = "tree-sitter")]
1483mod tests {
1484    use super::*;
1485
1486    #[test]
1487    fn ts_named_import() {
1488        let src = r#"import { useState, useEffect } from 'react';"#;
1489        let analysis = analyze(src, "ts");
1490        assert_eq!(analysis.imports.len(), 1);
1491        assert_eq!(analysis.imports[0].source, "react");
1492        assert_eq!(analysis.imports[0].names, vec!["useState", "useEffect"]);
1493    }
1494
1495    #[test]
1496    fn ts_default_import() {
1497        let src = r#"import React from 'react';"#;
1498        let analysis = analyze(src, "ts");
1499        assert_eq!(analysis.imports.len(), 1);
1500        assert_eq!(analysis.imports[0].kind, ImportKind::Default);
1501        assert_eq!(analysis.imports[0].names, vec!["React"]);
1502    }
1503
1504    #[test]
1505    fn ts_star_import() {
1506        let src = r#"import * as path from 'path';"#;
1507        let analysis = analyze(src, "ts");
1508        assert_eq!(analysis.imports.len(), 1);
1509        assert_eq!(analysis.imports[0].kind, ImportKind::Star);
1510    }
1511
1512    #[test]
1513    fn ts_side_effect_import() {
1514        let src = r#"import './styles.css';"#;
1515        let analysis = analyze(src, "ts");
1516        assert_eq!(analysis.imports.len(), 1);
1517        assert_eq!(analysis.imports[0].kind, ImportKind::SideEffect);
1518        assert_eq!(analysis.imports[0].source, "./styles.css");
1519    }
1520
1521    #[test]
1522    fn ts_type_only_import() {
1523        let src = r#"import type { User } from './types';"#;
1524        let analysis = analyze(src, "ts");
1525        assert_eq!(analysis.imports.len(), 1);
1526        assert!(analysis.imports[0].is_type_only);
1527    }
1528
1529    #[test]
1530    fn ts_reexport() {
1531        let src = r#"export { foo, bar } from './utils';"#;
1532        let analysis = analyze(src, "ts");
1533        assert_eq!(analysis.imports.len(), 1);
1534        assert_eq!(analysis.imports[0].kind, ImportKind::Reexport);
1535    }
1536
1537    #[test]
1538    fn ts_call_sites() {
1539        let src = r#"
1540const x = foo(1);
1541const y = obj.method(2);
1542"#;
1543        let analysis = analyze(src, "ts");
1544        assert!(analysis.calls.len() >= 2);
1545        let fns: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
1546        assert!(fns.contains(&"foo"));
1547        assert!(fns.contains(&"method"));
1548    }
1549
1550    #[test]
1551    fn ts_interface() {
1552        let src = r#"
1553export interface User {
1554    name: string;
1555    age: number;
1556}
1557"#;
1558        let analysis = analyze(src, "ts");
1559        assert_eq!(analysis.types.len(), 1);
1560        assert_eq!(analysis.types[0].name, "User");
1561        assert_eq!(analysis.types[0].kind, TypeDefKind::Interface);
1562    }
1563
1564    #[test]
1565    fn ts_type_alias_union() {
1566        let src = r#"type Result = Success | Error;"#;
1567        let analysis = analyze(src, "ts");
1568        assert_eq!(analysis.types.len(), 1);
1569        assert_eq!(analysis.types[0].kind, TypeDefKind::Union);
1570    }
1571
1572    #[test]
1573    fn rust_use_statements() {
1574        let src = r#"
1575use crate::core::session;
1576use anyhow::Result;
1577use std::collections::HashMap;
1578"#;
1579        let analysis = analyze(src, "rs");
1580        assert_eq!(analysis.imports.len(), 2);
1581        let sources: Vec<&str> = analysis.imports.iter().map(|i| i.source.as_str()).collect();
1582        assert!(sources.contains(&"crate::core::session"));
1583        assert!(sources.contains(&"anyhow::Result"));
1584    }
1585
1586    #[test]
1587    fn rust_pub_use_reexport() {
1588        let src = r#"pub use crate::tools::ctx_read;"#;
1589        let analysis = analyze(src, "rs");
1590        assert_eq!(analysis.imports.len(), 1);
1591        assert_eq!(analysis.imports[0].kind, ImportKind::Reexport);
1592    }
1593
1594    #[test]
1595    fn rust_struct_and_trait() {
1596        let src = r#"
1597pub struct Config {
1598    pub name: String,
1599}
1600
1601pub trait Service {
1602    fn run(&self);
1603}
1604"#;
1605        let analysis = analyze(src, "rs");
1606        assert_eq!(analysis.types.len(), 2);
1607        let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
1608        assert!(names.contains(&"Config"));
1609        assert!(names.contains(&"Service"));
1610    }
1611
1612    #[test]
1613    fn rust_call_sites() {
1614        let src = r#"
1615fn main() {
1616    let x = calculate(42);
1617    let y = self.process();
1618    Vec::new();
1619}
1620"#;
1621        let analysis = analyze(src, "rs");
1622        assert!(analysis.calls.len() >= 2);
1623        let fns: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
1624        assert!(fns.contains(&"calculate"));
1625    }
1626
1627    #[test]
1628    fn python_imports() {
1629        let src = r#"
1630import os
1631from pathlib import Path
1632from . import utils
1633from ..models import User, Role
1634"#;
1635        let analysis = analyze(src, "py");
1636        assert!(analysis.imports.len() >= 3);
1637    }
1638
1639    #[test]
1640    fn python_class_protocol() {
1641        let src = r#"
1642class MyProtocol(Protocol):
1643    def method(self) -> None: ...
1644
1645class User:
1646    name: str
1647"#;
1648        let analysis = analyze(src, "py");
1649        assert_eq!(analysis.types.len(), 2);
1650        assert_eq!(analysis.types[0].kind, TypeDefKind::Protocol);
1651        assert_eq!(analysis.types[1].kind, TypeDefKind::Class);
1652    }
1653
1654    #[test]
1655    fn go_imports() {
1656        let src = r#"
1657package main
1658
1659import (
1660    "fmt"
1661    "net/http"
1662    _ "github.com/lib/pq"
1663)
1664"#;
1665        let analysis = analyze(src, "go");
1666        assert!(analysis.imports.len() >= 3);
1667        let side_effect = analysis.imports.iter().find(|i| i.source.contains("pq"));
1668        assert!(side_effect.is_some());
1669        assert_eq!(side_effect.unwrap().kind, ImportKind::SideEffect);
1670    }
1671
1672    #[test]
1673    fn go_struct_and_interface() {
1674        let src = r#"
1675package main
1676
1677type Server struct {
1678    Port int
1679}
1680
1681type Handler interface {
1682    Handle(r *Request)
1683}
1684"#;
1685        let analysis = analyze(src, "go");
1686        assert_eq!(analysis.types.len(), 2);
1687        let kinds: Vec<&TypeDefKind> = analysis.types.iter().map(|t| &t.kind).collect();
1688        assert!(kinds.contains(&&TypeDefKind::Struct));
1689        assert!(kinds.contains(&&TypeDefKind::Interface));
1690    }
1691
1692    #[test]
1693    fn java_imports() {
1694        let src = r#"
1695import java.util.List;
1696import java.util.Map;
1697import static org.junit.Assert.*;
1698"#;
1699        let analysis = analyze(src, "java");
1700        assert!(analysis.imports.len() >= 2);
1701    }
1702
1703    #[test]
1704    fn java_class_and_interface() {
1705        let src = r#"
1706public class UserService {
1707    public void save(User u) {}
1708}
1709
1710public interface Repository<T> {
1711    T findById(int id);
1712}
1713
1714public enum Status { ACTIVE, INACTIVE }
1715
1716public record Point(int x, int y) {}
1717"#;
1718        let analysis = analyze(src, "java");
1719        assert!(analysis.types.len() >= 3);
1720        let kinds: Vec<&TypeDefKind> = analysis.types.iter().map(|t| &t.kind).collect();
1721        assert!(kinds.contains(&&TypeDefKind::Class));
1722        assert!(kinds.contains(&&TypeDefKind::Interface));
1723        assert!(kinds.contains(&&TypeDefKind::Enum));
1724    }
1725
1726    #[test]
1727    fn kotlin_imports_and_aliases() {
1728        let src = r#"
1729package com.example.app
1730
1731import com.example.services.UserService
1732import com.example.factories.WidgetFactory as Factory
1733import com.example.shared.*
1734"#;
1735        let analysis = analyze(src, "kt");
1736        assert_eq!(analysis.imports.len(), 3);
1737        assert_eq!(
1738            analysis.imports[0].source,
1739            "com.example.services.UserService"
1740        );
1741        assert_eq!(analysis.imports[1].names, vec!["Factory"]);
1742        assert_eq!(analysis.imports[2].kind, ImportKind::Star);
1743    }
1744
1745    #[test]
1746    fn kotlin_call_sites() {
1747        let src = r#"
1748class UserService {
1749    fun run() {
1750        prepare()
1751        repository.save(user)
1752        Factory.create()
1753    }
1754}
1755"#;
1756        let analysis = analyze(src, "kt");
1757        let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
1758        assert!(callees.contains(&"prepare"));
1759        assert!(callees.contains(&"save"));
1760        assert!(callees.contains(&"create"));
1761    }
1762
1763    #[test]
1764    fn kotlin_types_and_visibility() {
1765        let src = r#"
1766sealed interface Handler
1767data class User(val id: String)
1768enum class Status { ACTIVE, INACTIVE }
1769object Registry
1770private typealias UserId = String
1771"#;
1772        let analysis = analyze(src, "kt");
1773        let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
1774        assert!(names.contains(&"Handler"));
1775        assert!(names.contains(&"User"));
1776        assert!(names.contains(&"Status"));
1777        assert!(names.contains(&"Registry"));
1778        assert!(names.contains(&"UserId"));
1779        let handler = analysis.types.iter().find(|t| t.name == "Handler").unwrap();
1780        assert_eq!(handler.kind, TypeDefKind::Interface);
1781        let alias = analysis.types.iter().find(|t| t.name == "UserId").unwrap();
1782        assert!(!alias.is_exported);
1783    }
1784
1785    #[test]
1786    fn ts_generics_extracted() {
1787        let src = r#"interface Result<T, E> { ok: T; err: E; }"#;
1788        let analysis = analyze(src, "ts");
1789        assert_eq!(analysis.types.len(), 1);
1790        assert!(!analysis.types[0].generics.is_empty());
1791    }
1792
1793    #[test]
1794    fn mixed_analysis_ts() {
1795        let src = r#"
1796import { Request, Response } from 'express';
1797import type { User } from './models';
1798
1799export interface Handler {
1800    handle(req: Request): Response;
1801}
1802
1803export class Router {
1804    register(path: string, handler: Handler) {
1805        this.handlers.set(path, handler);
1806    }
1807}
1808
1809const app = express();
1810app.listen(3000);
1811"#;
1812        let analysis = analyze(src, "ts");
1813        assert!(analysis.imports.len() >= 2, "Should find imports");
1814        assert!(!analysis.types.is_empty(), "Should find types");
1815        assert!(!analysis.calls.is_empty(), "Should find calls");
1816    }
1817
1818    #[test]
1819    fn empty_file() {
1820        let analysis = analyze("", "ts");
1821        assert!(analysis.imports.is_empty());
1822        assert!(analysis.calls.is_empty());
1823        assert!(analysis.types.is_empty());
1824    }
1825
1826    #[test]
1827    fn unsupported_extension() {
1828        let analysis = analyze("some content", "txt");
1829        assert!(analysis.imports.is_empty());
1830    }
1831
1832    #[test]
1833    fn c_include_import() {
1834        let src = r#"
1835#include "foo/bar.h"
1836#include <stdio.h>
1837"#;
1838        let analysis = analyze(src, "c");
1839        assert!(analysis.imports.iter().any(|i| i.source == "foo/bar.h"));
1840    }
1841
1842    #[test]
1843    fn bash_source_import() {
1844        let src = r#"
1845source "./scripts/env.sh"
1846. ../common.sh
1847"#;
1848        let analysis = analyze(src, "sh");
1849        assert!(
1850            analysis
1851                .imports
1852                .iter()
1853                .any(|i| i.source.contains("scripts/env.sh")),
1854            "expected source import"
1855        );
1856    }
1857
1858    #[test]
1859    fn zig_at_import() {
1860        let src = r#"
1861const m = @import("lib/math.zig");
1862const std = @import("std");
1863"#;
1864        let analysis = analyze(src, "zig");
1865        assert!(analysis.imports.iter().any(|i| i.source == "lib/math.zig"));
1866    }
1867}