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