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        _ => None,
123    }
124}
125
126// ---------------------------------------------------------------------------
127// Imports
128// ---------------------------------------------------------------------------
129
130#[cfg(feature = "tree-sitter")]
131fn extract_imports(root: Node, src: &str, ext: &str) -> Vec<ImportInfo> {
132    match ext {
133        "ts" | "tsx" | "js" | "jsx" => extract_imports_ts(root, src),
134        "rs" => extract_imports_rust(root, src),
135        "py" => extract_imports_python(root, src),
136        "go" => extract_imports_go(root, src),
137        "java" => extract_imports_java(root, src),
138        _ => Vec::new(),
139    }
140}
141
142#[cfg(feature = "tree-sitter")]
143fn extract_imports_ts(root: Node, src: &str) -> Vec<ImportInfo> {
144    let mut imports = Vec::new();
145    let mut cursor = root.walk();
146
147    for node in root.children(&mut cursor) {
148        match node.kind() {
149            "import_statement" => {
150                if let Some(info) = parse_ts_import(node, src) {
151                    imports.push(info);
152                }
153            }
154            "export_statement" => {
155                if let Some(source) = find_child_by_kind(node, "string") {
156                    let source_text = unquote(node_text(source, src));
157                    let names = collect_named_imports(node, src);
158                    imports.push(ImportInfo {
159                        source: source_text,
160                        names,
161                        kind: ImportKind::Reexport,
162                        line: node.start_position().row + 1,
163                        is_type_only: false,
164                    });
165                }
166            }
167            _ => {}
168        }
169    }
170
171    walk_for_dynamic_imports(root, src, &mut imports);
172
173    imports
174}
175
176#[cfg(feature = "tree-sitter")]
177fn parse_ts_import(node: Node, src: &str) -> Option<ImportInfo> {
178    let source_node =
179        find_child_by_kind(node, "string").or_else(|| find_descendant_by_kind(node, "string"))?;
180    let source = unquote(node_text(source_node, src));
181
182    let is_type_only = node_text(node, src).starts_with("import type");
183
184    let clause = find_child_by_kind(node, "import_clause");
185    let (kind, names) = match clause {
186        Some(c) => classify_ts_import_clause(c, src),
187        None => (ImportKind::SideEffect, Vec::new()),
188    };
189
190    Some(ImportInfo {
191        source,
192        names,
193        kind,
194        line: node.start_position().row + 1,
195        is_type_only,
196    })
197}
198
199#[cfg(feature = "tree-sitter")]
200fn classify_ts_import_clause(clause: Node, src: &str) -> (ImportKind, Vec<String>) {
201    let mut names = Vec::new();
202    let mut has_default = false;
203    let mut has_star = false;
204
205    let mut cursor = clause.walk();
206    for child in clause.children(&mut cursor) {
207        match child.kind() {
208            "identifier" => {
209                has_default = true;
210                names.push(node_text(child, src).to_string());
211            }
212            "namespace_import" => {
213                has_star = true;
214                if let Some(id) = find_child_by_kind(child, "identifier") {
215                    names.push(format!("* as {}", node_text(id, src)));
216                }
217            }
218            "named_imports" => {
219                let mut inner = child.walk();
220                for spec in child.children(&mut inner) {
221                    if spec.kind() == "import_specifier" {
222                        let name = find_child_by_kind(spec, "identifier")
223                            .map(|n| node_text(n, src).to_string());
224                        if let Some(n) = name {
225                            names.push(n);
226                        }
227                    }
228                }
229            }
230            _ => {}
231        }
232    }
233
234    let kind = if has_star {
235        ImportKind::Star
236    } else if has_default && names.len() == 1 {
237        ImportKind::Default
238    } else {
239        ImportKind::Named
240    };
241
242    (kind, names)
243}
244
245#[cfg(feature = "tree-sitter")]
246fn walk_for_dynamic_imports(node: Node, src: &str, imports: &mut Vec<ImportInfo>) {
247    if node.kind() == "call_expression" {
248        let callee = find_child_by_kind(node, "import");
249        if callee.is_some() {
250            if let Some(args) = find_child_by_kind(node, "arguments") {
251                if let Some(first_arg) = find_child_by_kind(args, "string") {
252                    imports.push(ImportInfo {
253                        source: unquote(node_text(first_arg, src)),
254                        names: Vec::new(),
255                        kind: ImportKind::Dynamic,
256                        line: node.start_position().row + 1,
257                        is_type_only: false,
258                    });
259                }
260            }
261        }
262    }
263    let mut cursor = node.walk();
264    for child in node.children(&mut cursor) {
265        walk_for_dynamic_imports(child, src, imports);
266    }
267}
268
269#[cfg(feature = "tree-sitter")]
270fn extract_imports_rust(root: Node, src: &str) -> Vec<ImportInfo> {
271    let mut imports = Vec::new();
272    let mut cursor = root.walk();
273
274    for node in root.children(&mut cursor) {
275        if node.kind() == "mod_item" {
276            let text = node_text(node, src);
277            if !text.contains('{') {
278                if let Some(name_node) = find_child_by_kind(node, "identifier") {
279                    let mod_name = node_text(name_node, src).to_string();
280                    imports.push(ImportInfo {
281                        source: mod_name.clone(),
282                        names: vec![mod_name],
283                        kind: ImportKind::Named,
284                        line: node.start_position().row + 1,
285                        is_type_only: false,
286                    });
287                }
288            }
289        } else if node.kind() == "use_declaration" {
290            let is_pub = node_text(node, src).trim_start().starts_with("pub");
291            let kind = if is_pub {
292                ImportKind::Reexport
293            } else {
294                ImportKind::Named
295            };
296
297            if let Some(arg) = find_child_by_kind(node, "use_as_clause")
298                .or_else(|| find_child_by_kind(node, "scoped_identifier"))
299                .or_else(|| find_child_by_kind(node, "scoped_use_list"))
300                .or_else(|| find_child_by_kind(node, "use_wildcard"))
301                .or_else(|| find_child_by_kind(node, "identifier"))
302            {
303                let full_path = node_text(arg, src).to_string();
304
305                let (source, names) = if full_path.contains('{') {
306                    let parts: Vec<&str> = full_path.splitn(2, "::").collect();
307                    let base = parts[0].to_string();
308                    let items: Vec<String> = full_path
309                        .split('{')
310                        .nth(1)
311                        .unwrap_or("")
312                        .trim_end_matches('}')
313                        .split(',')
314                        .map(|s| s.trim().to_string())
315                        .filter(|s| !s.is_empty())
316                        .collect();
317                    (base, items)
318                } else if full_path.ends_with("::*") {
319                    (
320                        full_path.trim_end_matches("::*").to_string(),
321                        vec!["*".to_string()],
322                    )
323                } else {
324                    let name = full_path.rsplit("::").next().unwrap_or(&full_path);
325                    (full_path.clone(), vec![name.to_string()])
326                };
327
328                let is_std = source.starts_with("std")
329                    || source.starts_with("core")
330                    || source.starts_with("alloc");
331                if !is_std {
332                    imports.push(ImportInfo {
333                        source,
334                        names,
335                        kind: if full_path.contains('*') {
336                            ImportKind::Star
337                        } else {
338                            kind.clone()
339                        },
340                        line: node.start_position().row + 1,
341                        is_type_only: false,
342                    });
343                }
344            }
345        }
346    }
347
348    imports
349}
350
351#[cfg(feature = "tree-sitter")]
352fn extract_imports_python(root: Node, src: &str) -> Vec<ImportInfo> {
353    let mut imports = Vec::new();
354    let mut cursor = root.walk();
355
356    for node in root.children(&mut cursor) {
357        match node.kind() {
358            "import_statement" => {
359                let mut inner = node.walk();
360                for child in node.children(&mut inner) {
361                    if child.kind() == "dotted_name" || child.kind() == "aliased_import" {
362                        let text = node_text(child, src);
363                        let module = if child.kind() == "aliased_import" {
364                            find_child_by_kind(child, "dotted_name")
365                                .map(|n| node_text(n, src).to_string())
366                                .unwrap_or_else(|| text.to_string())
367                        } else {
368                            text.to_string()
369                        };
370                        imports.push(ImportInfo {
371                            source: module,
372                            names: Vec::new(),
373                            kind: ImportKind::Named,
374                            line: node.start_position().row + 1,
375                            is_type_only: false,
376                        });
377                    }
378                }
379            }
380            "import_from_statement" => {
381                let module = find_child_by_kind(node, "dotted_name")
382                    .or_else(|| find_child_by_kind(node, "relative_import"))
383                    .map(|n| node_text(n, src).to_string())
384                    .unwrap_or_default();
385
386                let mut names = Vec::new();
387                let mut is_star = false;
388
389                let mut inner = node.walk();
390                for child in node.children(&mut inner) {
391                    if child.kind() == "wildcard_import" {
392                        is_star = true;
393                    } else if child.kind() == "import_prefix" {
394                        // relative import dots handled via module already
395                    } else if child.kind() == "dotted_name"
396                        && child.start_position() != node.start_position()
397                    {
398                        names.push(node_text(child, src).to_string());
399                    } else if child.kind() == "aliased_import" {
400                        if let Some(n) = find_child_by_kind(child, "dotted_name")
401                            .or_else(|| find_child_by_kind(child, "identifier"))
402                        {
403                            names.push(node_text(n, src).to_string());
404                        }
405                    }
406                }
407
408                imports.push(ImportInfo {
409                    source: module,
410                    names,
411                    kind: if is_star {
412                        ImportKind::Star
413                    } else {
414                        ImportKind::Named
415                    },
416                    line: node.start_position().row + 1,
417                    is_type_only: false,
418                });
419            }
420            _ => {}
421        }
422    }
423
424    imports
425}
426
427#[cfg(feature = "tree-sitter")]
428fn extract_imports_go(root: Node, src: &str) -> Vec<ImportInfo> {
429    let mut imports = Vec::new();
430    let mut cursor = root.walk();
431
432    for node in root.children(&mut cursor) {
433        if node.kind() == "import_declaration" {
434            let mut inner = node.walk();
435            for child in node.children(&mut inner) {
436                match child.kind() {
437                    "import_spec" => {
438                        if let Some(path_node) =
439                            find_child_by_kind(child, "interpreted_string_literal")
440                        {
441                            let source = unquote(node_text(path_node, src));
442                            let alias = find_child_by_kind(child, "package_identifier")
443                                .or_else(|| find_child_by_kind(child, "dot"))
444                                .or_else(|| find_child_by_kind(child, "blank_identifier"));
445                            let kind = match alias.map(|a| node_text(a, src)) {
446                                Some(".") => ImportKind::Star,
447                                Some("_") => ImportKind::SideEffect,
448                                _ => ImportKind::Named,
449                            };
450                            imports.push(ImportInfo {
451                                source,
452                                names: Vec::new(),
453                                kind,
454                                line: child.start_position().row + 1,
455                                is_type_only: false,
456                            });
457                        }
458                    }
459                    "import_spec_list" => {
460                        let mut spec_cursor = child.walk();
461                        for spec in child.children(&mut spec_cursor) {
462                            if spec.kind() == "import_spec" {
463                                if let Some(path_node) =
464                                    find_child_by_kind(spec, "interpreted_string_literal")
465                                {
466                                    let source = unquote(node_text(path_node, src));
467                                    let alias = find_child_by_kind(spec, "package_identifier")
468                                        .or_else(|| find_child_by_kind(spec, "dot"))
469                                        .or_else(|| find_child_by_kind(spec, "blank_identifier"));
470                                    let kind = match alias.map(|a| node_text(a, src)) {
471                                        Some(".") => ImportKind::Star,
472                                        Some("_") => ImportKind::SideEffect,
473                                        _ => ImportKind::Named,
474                                    };
475                                    imports.push(ImportInfo {
476                                        source,
477                                        names: Vec::new(),
478                                        kind,
479                                        line: spec.start_position().row + 1,
480                                        is_type_only: false,
481                                    });
482                                }
483                            }
484                        }
485                    }
486                    "interpreted_string_literal" => {
487                        let source = unquote(node_text(child, src));
488                        imports.push(ImportInfo {
489                            source,
490                            names: Vec::new(),
491                            kind: ImportKind::Named,
492                            line: child.start_position().row + 1,
493                            is_type_only: false,
494                        });
495                    }
496                    _ => {}
497                }
498            }
499        }
500    }
501
502    imports
503}
504
505#[cfg(feature = "tree-sitter")]
506fn extract_imports_java(root: Node, src: &str) -> Vec<ImportInfo> {
507    let mut imports = Vec::new();
508    let mut cursor = root.walk();
509
510    for node in root.children(&mut cursor) {
511        if node.kind() == "import_declaration" {
512            let text = node_text(node, src).to_string();
513            let _is_static = text.contains("static ");
514
515            let path_node = find_child_by_kind(node, "scoped_identifier")
516                .or_else(|| find_child_by_kind(node, "identifier"));
517            if let Some(p) = path_node {
518                let full_path = node_text(p, src).to_string();
519
520                let is_wildcard = find_child_by_kind(node, "asterisk").is_some();
521                let kind = if is_wildcard {
522                    ImportKind::Star
523                } else {
524                    ImportKind::Named
525                };
526
527                let name = full_path
528                    .rsplit('.')
529                    .next()
530                    .unwrap_or(&full_path)
531                    .to_string();
532                imports.push(ImportInfo {
533                    source: full_path,
534                    names: vec![name],
535                    kind,
536                    line: node.start_position().row + 1,
537                    is_type_only: false,
538                });
539            }
540        }
541    }
542
543    imports
544}
545
546// ---------------------------------------------------------------------------
547// Call Sites
548// ---------------------------------------------------------------------------
549
550#[cfg(feature = "tree-sitter")]
551fn extract_calls(root: Node, src: &str, ext: &str) -> Vec<CallSite> {
552    let mut calls = Vec::new();
553    walk_calls(root, src, ext, &mut calls);
554    calls
555}
556
557#[cfg(feature = "tree-sitter")]
558fn walk_calls(node: Node, src: &str, ext: &str, calls: &mut Vec<CallSite>) {
559    if node.kind() == "call_expression" || node.kind() == "method_invocation" {
560        if let Some(call) = parse_call(node, src, ext) {
561            calls.push(call);
562        }
563    }
564
565    let mut cursor = node.walk();
566    for child in node.children(&mut cursor) {
567        walk_calls(child, src, ext, calls);
568    }
569}
570
571#[cfg(feature = "tree-sitter")]
572fn parse_call(node: Node, src: &str, ext: &str) -> Option<CallSite> {
573    match ext {
574        "ts" | "tsx" | "js" | "jsx" => parse_call_ts(node, src),
575        "rs" => parse_call_rust(node, src),
576        "py" => parse_call_python(node, src),
577        "go" => parse_call_go(node, src),
578        "java" => parse_call_java(node, src),
579        _ => None,
580    }
581}
582
583#[cfg(feature = "tree-sitter")]
584fn parse_call_ts(node: Node, src: &str) -> Option<CallSite> {
585    let func = find_child_by_kind(node, "member_expression")
586        .or_else(|| find_child_by_kind(node, "identifier"))
587        .or_else(|| find_child_by_kind(node, "subscript_expression"))?;
588
589    if func.kind() == "member_expression" {
590        let obj =
591            find_child_by_kind(func, "identifier").or_else(|| find_child_by_kind(func, "this"))?;
592        let prop = find_child_by_kind(func, "property_identifier")?;
593        Some(CallSite {
594            callee: node_text(prop, src).to_string(),
595            line: node.start_position().row + 1,
596            col: node.start_position().column,
597            receiver: Some(node_text(obj, src).to_string()),
598            is_method: true,
599        })
600    } else {
601        Some(CallSite {
602            callee: node_text(func, src).to_string(),
603            line: node.start_position().row + 1,
604            col: node.start_position().column,
605            receiver: None,
606            is_method: false,
607        })
608    }
609}
610
611#[cfg(feature = "tree-sitter")]
612fn parse_call_rust(node: Node, src: &str) -> Option<CallSite> {
613    let func = node.child(0)?;
614    match func.kind() {
615        "field_expression" => {
616            let field = find_child_by_kind(func, "field_identifier")?;
617            let receiver = func.child(0).map(|r| node_text(r, src).to_string());
618            Some(CallSite {
619                callee: node_text(field, src).to_string(),
620                line: node.start_position().row + 1,
621                col: node.start_position().column,
622                receiver,
623                is_method: true,
624            })
625        }
626        "scoped_identifier" | "identifier" => Some(CallSite {
627            callee: node_text(func, src).to_string(),
628            line: node.start_position().row + 1,
629            col: node.start_position().column,
630            receiver: None,
631            is_method: false,
632        }),
633        _ => None,
634    }
635}
636
637#[cfg(feature = "tree-sitter")]
638fn parse_call_python(node: Node, src: &str) -> Option<CallSite> {
639    let func = node.child(0)?;
640    match func.kind() {
641        "attribute" => {
642            let attr = find_child_by_kind(func, "identifier");
643            let obj = func.child(0).map(|r| node_text(r, src).to_string());
644            let name = attr
645                .map(|a| node_text(a, src).to_string())
646                .or_else(|| {
647                    let text = node_text(func, src);
648                    text.rsplit('.').next().map(|s| s.to_string())
649                })
650                .unwrap_or_default();
651            Some(CallSite {
652                callee: name,
653                line: node.start_position().row + 1,
654                col: node.start_position().column,
655                receiver: obj,
656                is_method: true,
657            })
658        }
659        "identifier" => Some(CallSite {
660            callee: node_text(func, src).to_string(),
661            line: node.start_position().row + 1,
662            col: node.start_position().column,
663            receiver: None,
664            is_method: false,
665        }),
666        _ => None,
667    }
668}
669
670#[cfg(feature = "tree-sitter")]
671fn parse_call_go(node: Node, src: &str) -> Option<CallSite> {
672    let func = node.child(0)?;
673    match func.kind() {
674        "selector_expression" => {
675            let field = find_child_by_kind(func, "field_identifier")?;
676            let obj = func.child(0).map(|r| node_text(r, src).to_string());
677            Some(CallSite {
678                callee: node_text(field, src).to_string(),
679                line: node.start_position().row + 1,
680                col: node.start_position().column,
681                receiver: obj,
682                is_method: true,
683            })
684        }
685        "identifier" => Some(CallSite {
686            callee: node_text(func, src).to_string(),
687            line: node.start_position().row + 1,
688            col: node.start_position().column,
689            receiver: None,
690            is_method: false,
691        }),
692        _ => None,
693    }
694}
695
696#[cfg(feature = "tree-sitter")]
697fn parse_call_java(node: Node, src: &str) -> Option<CallSite> {
698    if node.kind() == "method_invocation" {
699        let name = find_child_by_kind(node, "identifier")?;
700        let obj = find_child_by_kind(node, "field_access")
701            .or_else(|| {
702                let first = node.child(0)?;
703                if first.kind() == "identifier" && first.id() != name.id() {
704                    Some(first)
705                } else {
706                    None
707                }
708            })
709            .map(|o| node_text(o, src).to_string());
710        return Some(CallSite {
711            callee: node_text(name, src).to_string(),
712            line: node.start_position().row + 1,
713            col: node.start_position().column,
714            receiver: obj,
715            is_method: true,
716        });
717    }
718
719    let func = node.child(0)?;
720    Some(CallSite {
721        callee: node_text(func, src).to_string(),
722        line: node.start_position().row + 1,
723        col: node.start_position().column,
724        receiver: None,
725        is_method: false,
726    })
727}
728
729// ---------------------------------------------------------------------------
730// Type Definitions
731// ---------------------------------------------------------------------------
732
733#[cfg(feature = "tree-sitter")]
734fn extract_types(root: Node, src: &str, ext: &str) -> Vec<TypeDef> {
735    let mut types = Vec::new();
736    walk_types(root, src, ext, &mut types, false);
737    types
738}
739
740#[cfg(feature = "tree-sitter")]
741fn walk_types(node: Node, src: &str, ext: &str, types: &mut Vec<TypeDef>, parent_exported: bool) {
742    let exported = parent_exported || is_exported_node(node, src, ext);
743
744    if let Some(td) = match_type_def(node, src, ext, exported) {
745        types.push(td);
746    }
747
748    let mut cursor = node.walk();
749    for child in node.children(&mut cursor) {
750        walk_types(child, src, ext, types, exported);
751    }
752}
753
754#[cfg(feature = "tree-sitter")]
755fn match_type_def(node: Node, src: &str, ext: &str, parent_exported: bool) -> Option<TypeDef> {
756    let (name, kind) = match ext {
757        "ts" | "tsx" | "js" | "jsx" => match_type_def_ts(node, src)?,
758        "rs" => match_type_def_rust(node, src)?,
759        "py" => match_type_def_python(node, src)?,
760        "go" => match_type_def_go(node, src)?,
761        "java" => match_type_def_java(node, src)?,
762        _ => return None,
763    };
764
765    let is_exported = parent_exported || is_exported_node(node, src, ext);
766    let generics = extract_generics(node, src);
767
768    Some(TypeDef {
769        name,
770        kind,
771        line: node.start_position().row + 1,
772        end_line: node.end_position().row + 1,
773        is_exported,
774        generics,
775    })
776}
777
778#[cfg(feature = "tree-sitter")]
779fn match_type_def_ts(node: Node, src: &str) -> Option<(String, TypeDefKind)> {
780    match node.kind() {
781        "class_declaration" | "abstract_class_declaration" => {
782            let name = find_child_by_kind(node, "type_identifier")
783                .or_else(|| find_child_by_kind(node, "identifier"))?;
784            Some((node_text(name, src).to_string(), TypeDefKind::Class))
785        }
786        "interface_declaration" => {
787            let name = find_child_by_kind(node, "type_identifier")?;
788            Some((node_text(name, src).to_string(), TypeDefKind::Interface))
789        }
790        "type_alias_declaration" => {
791            let name = find_child_by_kind(node, "type_identifier")?;
792            let text = node_text(node, src);
793            let kind = if text.contains(" | ") {
794                TypeDefKind::Union
795            } else {
796                TypeDefKind::TypeAlias
797            };
798            Some((node_text(name, src).to_string(), kind))
799        }
800        "enum_declaration" => {
801            let name = find_child_by_kind(node, "identifier")?;
802            Some((node_text(name, src).to_string(), TypeDefKind::Enum))
803        }
804        _ => None,
805    }
806}
807
808#[cfg(feature = "tree-sitter")]
809fn match_type_def_rust(node: Node, src: &str) -> Option<(String, TypeDefKind)> {
810    match node.kind() {
811        "struct_item" => {
812            let name = find_child_by_kind(node, "type_identifier")?;
813            Some((node_text(name, src).to_string(), TypeDefKind::Struct))
814        }
815        "enum_item" => {
816            let name = find_child_by_kind(node, "type_identifier")?;
817            Some((node_text(name, src).to_string(), TypeDefKind::Enum))
818        }
819        "trait_item" => {
820            let name = find_child_by_kind(node, "type_identifier")?;
821            Some((node_text(name, src).to_string(), TypeDefKind::Trait))
822        }
823        "type_item" => {
824            let name = find_child_by_kind(node, "type_identifier")?;
825            Some((node_text(name, src).to_string(), TypeDefKind::TypeAlias))
826        }
827        _ => None,
828    }
829}
830
831#[cfg(feature = "tree-sitter")]
832fn match_type_def_python(node: Node, src: &str) -> Option<(String, TypeDefKind)> {
833    if node.kind() == "class_definition" {
834        let name = find_child_by_kind(node, "identifier")?;
835        let text = node_text(node, src);
836        let kind = if text.contains("Protocol") {
837            TypeDefKind::Protocol
838        } else if text.contains("TypedDict") || text.contains("@dataclass") {
839            TypeDefKind::Struct
840        } else if text.contains("Enum") {
841            TypeDefKind::Enum
842        } else {
843            TypeDefKind::Class
844        };
845        Some((node_text(name, src).to_string(), kind))
846    } else {
847        None
848    }
849}
850
851#[cfg(feature = "tree-sitter")]
852fn match_type_def_go(node: Node, src: &str) -> Option<(String, TypeDefKind)> {
853    if node.kind() == "type_spec" {
854        let name = find_child_by_kind(node, "type_identifier")?;
855        let count = node.child_count();
856        let type_body = node.child((count.saturating_sub(1)) as u32)?;
857        let kind = match type_body.kind() {
858            "struct_type" => TypeDefKind::Struct,
859            "interface_type" => TypeDefKind::Interface,
860            _ => TypeDefKind::TypeAlias,
861        };
862        Some((node_text(name, src).to_string(), kind))
863    } else {
864        None
865    }
866}
867
868#[cfg(feature = "tree-sitter")]
869fn match_type_def_java(node: Node, src: &str) -> Option<(String, TypeDefKind)> {
870    match node.kind() {
871        "class_declaration" => {
872            let name = find_child_by_kind(node, "identifier")?;
873            Some((node_text(name, src).to_string(), TypeDefKind::Class))
874        }
875        "interface_declaration" => {
876            let name = find_child_by_kind(node, "identifier")?;
877            Some((node_text(name, src).to_string(), TypeDefKind::Interface))
878        }
879        "enum_declaration" => {
880            let name = find_child_by_kind(node, "identifier")?;
881            Some((node_text(name, src).to_string(), TypeDefKind::Enum))
882        }
883        "record_declaration" => {
884            let name = find_child_by_kind(node, "identifier")?;
885            Some((node_text(name, src).to_string(), TypeDefKind::Record))
886        }
887        "annotation_type_declaration" => {
888            let name = find_child_by_kind(node, "identifier")?;
889            Some((node_text(name, src).to_string(), TypeDefKind::Annotation))
890        }
891        _ => None,
892    }
893}
894
895// ---------------------------------------------------------------------------
896// Exports
897// ---------------------------------------------------------------------------
898
899#[cfg(feature = "tree-sitter")]
900fn extract_exports(root: Node, src: &str, ext: &str) -> Vec<String> {
901    let mut exports = Vec::new();
902    walk_exports(root, src, ext, &mut exports);
903    exports
904}
905
906#[cfg(feature = "tree-sitter")]
907fn walk_exports(node: Node, src: &str, ext: &str, exports: &mut Vec<String>) {
908    if is_exported_node(node, src, ext) {
909        if let Some(name) = get_declaration_name(node, src) {
910            exports.push(name);
911        }
912    }
913    let mut cursor = node.walk();
914    for child in node.children(&mut cursor) {
915        walk_exports(child, src, ext, exports);
916    }
917}
918
919#[cfg(feature = "tree-sitter")]
920fn is_exported_node(node: Node, src: &str, ext: &str) -> bool {
921    match ext {
922        "ts" | "tsx" | "js" | "jsx" => {
923            node.kind() == "export_statement"
924                || node
925                    .parent()
926                    .is_some_and(|p| p.kind() == "export_statement")
927        }
928        "rs" => node_text(node, src).trim_start().starts_with("pub "),
929        "go" => {
930            if let Some(name) = get_declaration_name(node, src) {
931                name.starts_with(char::is_uppercase)
932            } else {
933                false
934            }
935        }
936        "java" => node_text(node, src).trim_start().starts_with("public "),
937        "py" => {
938            if let Some(name) = get_declaration_name(node, src) {
939                !name.starts_with('_')
940            } else {
941                false
942            }
943        }
944        _ => false,
945    }
946}
947
948#[cfg(feature = "tree-sitter")]
949fn get_declaration_name(node: Node, src: &str) -> Option<String> {
950    for kind in &[
951        "identifier",
952        "type_identifier",
953        "property_identifier",
954        "field_identifier",
955    ] {
956        if let Some(name_node) = find_child_by_kind(node, kind) {
957            return Some(node_text(name_node, src).to_string());
958        }
959    }
960    None
961}
962
963#[cfg(feature = "tree-sitter")]
964fn extract_generics(node: Node, src: &str) -> Vec<String> {
965    let tp = find_child_by_kind(node, "type_parameters")
966        .or_else(|| find_child_by_kind(node, "type_parameter_list"));
967    match tp {
968        Some(params) => {
969            let mut result = Vec::new();
970            let mut cursor = params.walk();
971            for child in params.children(&mut cursor) {
972                if child.kind() == "type_parameter"
973                    || child.kind() == "type_identifier"
974                    || child.kind() == "identifier"
975                {
976                    result.push(node_text(child, src).to_string());
977                }
978            }
979            result
980        }
981        None => Vec::new(),
982    }
983}
984
985// ---------------------------------------------------------------------------
986// Helpers
987// ---------------------------------------------------------------------------
988
989#[cfg(feature = "tree-sitter")]
990fn node_text<'a>(node: Node, src: &'a str) -> &'a str {
991    &src[node.byte_range()]
992}
993
994#[cfg(feature = "tree-sitter")]
995fn find_child_by_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
996    let mut cursor = node.walk();
997    let result = node.children(&mut cursor).find(|c| c.kind() == kind);
998    result
999}
1000
1001#[cfg(feature = "tree-sitter")]
1002fn find_descendant_by_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1003    if node.kind() == kind {
1004        return Some(node);
1005    }
1006    let mut cursor = node.walk();
1007    for child in node.children(&mut cursor) {
1008        if let Some(found) = find_descendant_by_kind(child, kind) {
1009            return Some(found);
1010        }
1011    }
1012    None
1013}
1014
1015#[cfg(feature = "tree-sitter")]
1016fn collect_named_imports(node: Node, src: &str) -> Vec<String> {
1017    let mut names = Vec::new();
1018    if let Some(named) = find_descendant_by_kind(node, "named_imports") {
1019        let mut cursor = named.walk();
1020        for child in named.children(&mut cursor) {
1021            if child.kind() == "import_specifier" || child.kind() == "export_specifier" {
1022                if let Some(id) = find_child_by_kind(child, "identifier") {
1023                    names.push(node_text(id, src).to_string());
1024                }
1025            }
1026        }
1027    }
1028    names
1029}
1030
1031fn unquote(s: &str) -> String {
1032    s.trim_matches(|c| c == '\'' || c == '"' || c == '`')
1033        .to_string()
1034}
1035
1036// ---------------------------------------------------------------------------
1037// Tests
1038// ---------------------------------------------------------------------------
1039
1040#[cfg(test)]
1041#[cfg(feature = "tree-sitter")]
1042mod tests {
1043    use super::*;
1044
1045    #[test]
1046    fn ts_named_import() {
1047        let src = r#"import { useState, useEffect } from 'react';"#;
1048        let analysis = analyze(src, "ts");
1049        assert_eq!(analysis.imports.len(), 1);
1050        assert_eq!(analysis.imports[0].source, "react");
1051        assert_eq!(analysis.imports[0].names, vec!["useState", "useEffect"]);
1052    }
1053
1054    #[test]
1055    fn ts_default_import() {
1056        let src = r#"import React from 'react';"#;
1057        let analysis = analyze(src, "ts");
1058        assert_eq!(analysis.imports.len(), 1);
1059        assert_eq!(analysis.imports[0].kind, ImportKind::Default);
1060        assert_eq!(analysis.imports[0].names, vec!["React"]);
1061    }
1062
1063    #[test]
1064    fn ts_star_import() {
1065        let src = r#"import * as path from 'path';"#;
1066        let analysis = analyze(src, "ts");
1067        assert_eq!(analysis.imports.len(), 1);
1068        assert_eq!(analysis.imports[0].kind, ImportKind::Star);
1069    }
1070
1071    #[test]
1072    fn ts_side_effect_import() {
1073        let src = r#"import './styles.css';"#;
1074        let analysis = analyze(src, "ts");
1075        assert_eq!(analysis.imports.len(), 1);
1076        assert_eq!(analysis.imports[0].kind, ImportKind::SideEffect);
1077        assert_eq!(analysis.imports[0].source, "./styles.css");
1078    }
1079
1080    #[test]
1081    fn ts_type_only_import() {
1082        let src = r#"import type { User } from './types';"#;
1083        let analysis = analyze(src, "ts");
1084        assert_eq!(analysis.imports.len(), 1);
1085        assert!(analysis.imports[0].is_type_only);
1086    }
1087
1088    #[test]
1089    fn ts_reexport() {
1090        let src = r#"export { foo, bar } from './utils';"#;
1091        let analysis = analyze(src, "ts");
1092        assert_eq!(analysis.imports.len(), 1);
1093        assert_eq!(analysis.imports[0].kind, ImportKind::Reexport);
1094    }
1095
1096    #[test]
1097    fn ts_call_sites() {
1098        let src = r#"
1099const x = foo(1);
1100const y = obj.method(2);
1101"#;
1102        let analysis = analyze(src, "ts");
1103        assert!(analysis.calls.len() >= 2);
1104        let fns: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
1105        assert!(fns.contains(&"foo"));
1106        assert!(fns.contains(&"method"));
1107    }
1108
1109    #[test]
1110    fn ts_interface() {
1111        let src = r#"
1112export interface User {
1113    name: string;
1114    age: number;
1115}
1116"#;
1117        let analysis = analyze(src, "ts");
1118        assert_eq!(analysis.types.len(), 1);
1119        assert_eq!(analysis.types[0].name, "User");
1120        assert_eq!(analysis.types[0].kind, TypeDefKind::Interface);
1121    }
1122
1123    #[test]
1124    fn ts_type_alias_union() {
1125        let src = r#"type Result = Success | Error;"#;
1126        let analysis = analyze(src, "ts");
1127        assert_eq!(analysis.types.len(), 1);
1128        assert_eq!(analysis.types[0].kind, TypeDefKind::Union);
1129    }
1130
1131    #[test]
1132    fn rust_use_statements() {
1133        let src = r#"
1134use crate::core::session;
1135use anyhow::Result;
1136use std::collections::HashMap;
1137"#;
1138        let analysis = analyze(src, "rs");
1139        assert_eq!(analysis.imports.len(), 2);
1140        let sources: Vec<&str> = analysis.imports.iter().map(|i| i.source.as_str()).collect();
1141        assert!(sources.contains(&"crate::core::session"));
1142        assert!(sources.contains(&"anyhow::Result"));
1143    }
1144
1145    #[test]
1146    fn rust_pub_use_reexport() {
1147        let src = r#"pub use crate::tools::ctx_read;"#;
1148        let analysis = analyze(src, "rs");
1149        assert_eq!(analysis.imports.len(), 1);
1150        assert_eq!(analysis.imports[0].kind, ImportKind::Reexport);
1151    }
1152
1153    #[test]
1154    fn rust_struct_and_trait() {
1155        let src = r#"
1156pub struct Config {
1157    pub name: String,
1158}
1159
1160pub trait Service {
1161    fn run(&self);
1162}
1163"#;
1164        let analysis = analyze(src, "rs");
1165        assert_eq!(analysis.types.len(), 2);
1166        let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
1167        assert!(names.contains(&"Config"));
1168        assert!(names.contains(&"Service"));
1169    }
1170
1171    #[test]
1172    fn rust_call_sites() {
1173        let src = r#"
1174fn main() {
1175    let x = calculate(42);
1176    let y = self.process();
1177    Vec::new();
1178}
1179"#;
1180        let analysis = analyze(src, "rs");
1181        assert!(analysis.calls.len() >= 2);
1182        let fns: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
1183        assert!(fns.contains(&"calculate"));
1184    }
1185
1186    #[test]
1187    fn python_imports() {
1188        let src = r#"
1189import os
1190from pathlib import Path
1191from . import utils
1192from ..models import User, Role
1193"#;
1194        let analysis = analyze(src, "py");
1195        assert!(analysis.imports.len() >= 3);
1196    }
1197
1198    #[test]
1199    fn python_class_protocol() {
1200        let src = r#"
1201class MyProtocol(Protocol):
1202    def method(self) -> None: ...
1203
1204class User:
1205    name: str
1206"#;
1207        let analysis = analyze(src, "py");
1208        assert_eq!(analysis.types.len(), 2);
1209        assert_eq!(analysis.types[0].kind, TypeDefKind::Protocol);
1210        assert_eq!(analysis.types[1].kind, TypeDefKind::Class);
1211    }
1212
1213    #[test]
1214    fn go_imports() {
1215        let src = r#"
1216package main
1217
1218import (
1219    "fmt"
1220    "net/http"
1221    _ "github.com/lib/pq"
1222)
1223"#;
1224        let analysis = analyze(src, "go");
1225        assert!(analysis.imports.len() >= 3);
1226        let side_effect = analysis.imports.iter().find(|i| i.source.contains("pq"));
1227        assert!(side_effect.is_some());
1228        assert_eq!(side_effect.unwrap().kind, ImportKind::SideEffect);
1229    }
1230
1231    #[test]
1232    fn go_struct_and_interface() {
1233        let src = r#"
1234package main
1235
1236type Server struct {
1237    Port int
1238}
1239
1240type Handler interface {
1241    Handle(r *Request)
1242}
1243"#;
1244        let analysis = analyze(src, "go");
1245        assert_eq!(analysis.types.len(), 2);
1246        let kinds: Vec<&TypeDefKind> = analysis.types.iter().map(|t| &t.kind).collect();
1247        assert!(kinds.contains(&&TypeDefKind::Struct));
1248        assert!(kinds.contains(&&TypeDefKind::Interface));
1249    }
1250
1251    #[test]
1252    fn java_imports() {
1253        let src = r#"
1254import java.util.List;
1255import java.util.Map;
1256import static org.junit.Assert.*;
1257"#;
1258        let analysis = analyze(src, "java");
1259        assert!(analysis.imports.len() >= 2);
1260    }
1261
1262    #[test]
1263    fn java_class_and_interface() {
1264        let src = r#"
1265public class UserService {
1266    public void save(User u) {}
1267}
1268
1269public interface Repository<T> {
1270    T findById(int id);
1271}
1272
1273public enum Status { ACTIVE, INACTIVE }
1274
1275public record Point(int x, int y) {}
1276"#;
1277        let analysis = analyze(src, "java");
1278        assert!(analysis.types.len() >= 3);
1279        let kinds: Vec<&TypeDefKind> = analysis.types.iter().map(|t| &t.kind).collect();
1280        assert!(kinds.contains(&&TypeDefKind::Class));
1281        assert!(kinds.contains(&&TypeDefKind::Interface));
1282        assert!(kinds.contains(&&TypeDefKind::Enum));
1283    }
1284
1285    #[test]
1286    fn ts_generics_extracted() {
1287        let src = r#"interface Result<T, E> { ok: T; err: E; }"#;
1288        let analysis = analyze(src, "ts");
1289        assert_eq!(analysis.types.len(), 1);
1290        assert!(!analysis.types[0].generics.is_empty());
1291    }
1292
1293    #[test]
1294    fn mixed_analysis_ts() {
1295        let src = r#"
1296import { Request, Response } from 'express';
1297import type { User } from './models';
1298
1299export interface Handler {
1300    handle(req: Request): Response;
1301}
1302
1303export class Router {
1304    register(path: string, handler: Handler) {
1305        this.handlers.set(path, handler);
1306    }
1307}
1308
1309const app = express();
1310app.listen(3000);
1311"#;
1312        let analysis = analyze(src, "ts");
1313        assert!(analysis.imports.len() >= 2, "Should find imports");
1314        assert!(!analysis.types.is_empty(), "Should find types");
1315        assert!(!analysis.calls.is_empty(), "Should find calls");
1316    }
1317
1318    #[test]
1319    fn empty_file() {
1320        let analysis = analyze("", "ts");
1321        assert!(analysis.imports.is_empty());
1322        assert!(analysis.calls.is_empty());
1323        assert!(analysis.types.is_empty());
1324    }
1325
1326    #[test]
1327    fn unsupported_extension() {
1328        let analysis = analyze("some content", "txt");
1329        assert!(analysis.imports.is_empty());
1330    }
1331}