1#[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#[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 } 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#[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#[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#[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#[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#[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}