1use serde::{Deserialize, Serialize};
6use serde_json::{json, Value};
7use std::collections::HashMap;
8use std::path::Path;
9
10fn default_language() -> String {
12 "python".to_string()
13}
14
15fn default_entry_type() -> String {
17 "file".to_string()
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct FunctionInfo {
23 #[serde(default)]
25 pub name: String,
26 #[serde(default)]
28 pub params: Vec<String>,
29 #[serde(default)]
31 pub return_type: Option<String>,
32 #[serde(default)]
34 pub docstring: Option<String>,
35 #[serde(default)]
37 pub is_method: bool,
38 #[serde(default)]
40 pub is_async: bool,
41 #[serde(default)]
43 pub decorators: Vec<String>,
44 #[serde(default)]
46 pub line_number: usize,
47 #[serde(default)]
49 pub end_line_number: Option<usize>,
50 #[serde(default = "default_language")]
52 pub language: String,
53}
54
55impl Default for FunctionInfo {
56 fn default() -> Self {
57 Self {
58 name: String::new(),
59 params: Vec::new(),
60 return_type: None,
61 docstring: None,
62 is_method: false,
63 is_async: false,
64 decorators: Vec::new(),
65 line_number: 0,
66 end_line_number: None,
67 language: default_language(),
68 }
69 }
70}
71
72impl FunctionInfo {
73 pub fn signature(&self) -> String {
75 let async_prefix = if self.is_async { "async " } else { "" };
76 let params = self.params.join(", ");
77
78 match self.language.as_str() {
79 "python" => {
80 let ret = self
81 .return_type
82 .as_ref()
83 .map(|r| format!(" -> {}", r))
84 .unwrap_or_default();
85 format!("{}def {}({}){}", async_prefix, self.name, params, ret)
86 }
87 "rust" => {
88 let ret = self
89 .return_type
90 .as_ref()
91 .map(|r| format!(" -> {}", r))
92 .unwrap_or_default();
93 format!("{}fn {}({}){}", async_prefix, self.name, params, ret)
94 }
95 "go" => {
96 let ret = self
97 .return_type
98 .as_ref()
99 .map(|r| format!(" {}", r))
100 .unwrap_or_default();
101 format!("func {}({}){}", self.name, params, ret)
102 }
103 "typescript" | "javascript" | "tsx" | "jsx" => {
104 let ret = self
105 .return_type
106 .as_ref()
107 .map(|r| format!(": {}", r))
108 .unwrap_or_default();
109 format!("{}function {}({}){}", async_prefix, self.name, params, ret)
110 }
111 "java" | "csharp" => {
112 let ret = self.return_type.as_deref().unwrap_or("void");
113 format!("{} {}({})", ret, self.name, params)
114 }
115 "c" | "cpp" => {
116 let ret = self.return_type.as_deref().unwrap_or("void");
117 format!("{} {}({})", ret, self.name, params)
118 }
119 "ruby" | "elixir" => {
120 format!("def {}({})", self.name, params)
121 }
122 "kotlin" => {
123 let ret = self
124 .return_type
125 .as_ref()
126 .map(|r| format!(": {}", r))
127 .unwrap_or_default();
128 format!("fun {}({}){}", self.name, params, ret)
129 }
130 "swift" => {
131 let ret = self
132 .return_type
133 .as_ref()
134 .map(|r| format!(" -> {}", r))
135 .unwrap_or_default();
136 format!("func {}({}){}", self.name, params, ret)
137 }
138 "scala" => {
139 let ret = self
140 .return_type
141 .as_ref()
142 .map(|r| format!(": {}", r))
143 .unwrap_or_default();
144 format!("def {}({}){}", self.name, params, ret)
145 }
146 "lua" => {
147 format!("function {}({})", self.name, params)
148 }
149 _ => {
151 let ret = self
152 .return_type
153 .as_ref()
154 .map(|r| format!(" -> {}", r))
155 .unwrap_or_default();
156 format!("{}def {}({}){}", async_prefix, self.name, params, ret)
157 }
158 }
159 }
160}
161
162#[derive(Debug, Clone, Default, Serialize, Deserialize)]
164pub struct FieldInfo {
165 #[serde(default)]
167 pub name: String,
168 #[serde(default)]
170 pub field_type: Option<String>,
171 #[serde(default)]
173 pub visibility: Option<String>,
174 #[serde(default)]
176 pub is_static: bool,
177 #[serde(default)]
179 pub is_final: bool,
180 #[serde(default)]
182 pub default_value: Option<String>,
183 #[serde(default)]
185 pub annotations: Vec<String>,
186 #[serde(default)]
188 pub line_number: usize,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct ClassInfo {
194 #[serde(default)]
196 pub name: String,
197 #[serde(default)]
199 pub bases: Vec<String>,
200 #[serde(default)]
202 pub docstring: Option<String>,
203 #[serde(default)]
205 pub methods: Vec<FunctionInfo>,
206 #[serde(default, skip_serializing_if = "Vec::is_empty")]
208 pub fields: Vec<FieldInfo>,
209 #[serde(default, skip_serializing_if = "Vec::is_empty")]
211 pub inner_classes: Vec<ClassInfo>,
212 #[serde(default)]
214 pub decorators: Vec<String>,
215 #[serde(default)]
217 pub line_number: usize,
218 #[serde(default)]
220 pub end_line_number: Option<usize>,
221 #[serde(default = "default_language")]
223 pub language: String,
224}
225
226impl Default for ClassInfo {
227 fn default() -> Self {
228 Self {
229 name: String::new(),
230 bases: Vec::new(),
231 docstring: None,
232 methods: Vec::new(),
233 fields: Vec::new(),
234 inner_classes: Vec::new(),
235 decorators: Vec::new(),
236 line_number: 0,
237 end_line_number: None,
238 language: default_language(),
239 }
240 }
241}
242
243impl ClassInfo {
244 pub fn signature(&self) -> String {
253 let bases_str = self.bases.join(", ");
254
255 match self.language.as_str() {
256 "typescript" | "javascript" | "tsx" | "jsx" => {
257 let mut sig = format!("class {}", self.name);
258 if let Some(first_base) = self.bases.first() {
259 sig.push_str(&format!(" extends {}", first_base));
260 }
261 sig
262 }
263 "go" => format!("type {} struct", self.name),
264 "rust" => format!("struct {}", self.name),
265 "java" | "kotlin" | "csharp" => {
266 if bases_str.is_empty() {
267 format!("class {}", self.name)
268 } else {
269 format!("class {} extends {}", self.name, bases_str)
270 }
271 }
272 _ => {
273 if bases_str.is_empty() {
275 format!("class {}", self.name)
276 } else {
277 format!("class {}({})", self.name, bases_str)
278 }
279 }
280 }
281 }
282}
283
284#[derive(Debug, Clone, Default, Serialize, Deserialize)]
286pub struct ImportInfo {
287 #[serde(default)]
289 pub module: String,
290 #[serde(default)]
292 pub names: Vec<String>,
293 #[serde(default)]
295 pub aliases: HashMap<String, String>,
296 #[serde(default)]
298 pub is_from: bool,
299 #[serde(default)]
301 pub level: usize,
302 #[serde(default)]
304 pub line_number: usize,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub visibility: Option<String>,
309}
310
311impl ImportInfo {
312 pub fn statement(&self) -> String {
363 if self.is_from {
364 let level_dots = ".".repeat(self.level);
365 let names_with_aliases: Vec<String> = self
366 .names
367 .iter()
368 .map(|name| {
369 if let Some(alias) = self.aliases.get(name) {
370 format!("{} as {}", name, alias)
371 } else {
372 name.clone()
373 }
374 })
375 .collect();
376
377 if self.module.is_empty() {
378 format!("from {} import {}", level_dots, names_with_aliases.join(", "))
379 } else {
380 format!(
381 "from {}{} import {}",
382 level_dots,
383 self.module,
384 names_with_aliases.join(", ")
385 )
386 }
387 } else {
388 if let Some(alias) = self.aliases.get(&self.module) {
390 format!("import {} as {}", self.module, alias)
391 } else {
392 format!("import {}", self.module)
393 }
394 }
395 }
396}
397
398#[derive(Debug, Clone, Default, Serialize, Deserialize)]
404pub struct CallGraphInfo {
405 pub calls: HashMap<String, Vec<String>>,
409
410 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
414 pub called_by: HashMap<String, Vec<String>>,
415}
416
417impl CallGraphInfo {
418 #[allow(dead_code)]
422 pub fn new() -> Self {
423 Self::default()
424 }
425
426 #[allow(dead_code)]
434 pub fn add_call(&mut self, caller: &str, callee: &str) {
435 let callees = self.calls.entry(caller.to_string()).or_default();
437 if !callees.contains(&callee.to_string()) {
438 callees.push(callee.to_string());
439 }
440
441 let callers = self.called_by.entry(callee.to_string()).or_default();
443 if !callers.contains(&caller.to_string()) {
444 callers.push(caller.to_string());
445 }
446 }
447
448 #[allow(dead_code)]
452 pub fn is_empty(&self) -> bool {
453 self.calls.is_empty()
454 }
455
456 #[allow(dead_code)]
460 pub fn get_callees(&self, caller: &str) -> Option<&Vec<String>> {
461 self.calls.get(caller)
462 }
463
464 #[allow(dead_code)]
468 pub fn get_callers(&self, callee: &str) -> Option<&Vec<String>> {
469 self.called_by.get(callee)
470 }
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize)]
475pub struct ModuleInfo {
476 #[serde(rename = "file_path", default)]
478 pub path: String,
479 #[serde(default = "default_language")]
481 pub language: String,
482 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub docstring: Option<String>,
485 #[serde(default)]
487 pub functions: Vec<FunctionInfo>,
488 #[serde(default)]
490 pub classes: Vec<ClassInfo>,
491 #[serde(default)]
493 pub imports: Vec<ImportInfo>,
494 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub call_graph: Option<CallGraphInfo>,
498}
499
500impl Default for ModuleInfo {
501 fn default() -> Self {
502 Self {
503 path: String::new(),
504 language: default_language(),
505 docstring: None,
506 functions: Vec::new(),
507 classes: Vec::new(),
508 imports: Vec::new(),
509 call_graph: None,
510 }
511 }
512}
513
514impl ModuleInfo {
515 #[allow(dead_code)]
523 pub fn to_dict(&self) -> Value {
524 let imports: Vec<Value> = self
525 .imports
526 .iter()
527 .map(|i| {
528 json!({
529 "module": i.module,
530 "names": i.names,
531 "aliases": i.aliases,
532 "is_from": i.is_from,
533 "level": i.level,
534 "line_number": i.line_number,
535 })
536 })
537 .collect();
538
539 let classes: Vec<Value> = self
540 .classes
541 .iter()
542 .map(|c| {
543 let methods: Vec<Value> = c
544 .methods
545 .iter()
546 .map(|m| {
547 json!({
548 "name": m.name,
549 "line_number": m.line_number,
550 "end_line_number": m.end_line_number,
551 "signature": m.signature(),
552 "params": m.params,
553 "return_type": m.return_type,
554 "docstring": m.docstring,
555 "is_async": m.is_async,
556 "decorators": m.decorators,
557 })
558 })
559 .collect();
560
561 json!({
562 "name": c.name,
563 "line_number": c.line_number,
564 "end_line_number": c.end_line_number,
565 "signature": c.signature(),
566 "bases": c.bases,
567 "docstring": c.docstring,
568 "decorators": c.decorators,
569 "methods": methods,
570 })
571 })
572 .collect();
573
574 let functions: Vec<Value> = self
575 .functions
576 .iter()
577 .map(|f| {
578 json!({
579 "name": f.name,
580 "line_number": f.line_number,
581 "end_line_number": f.end_line_number,
582 "signature": f.signature(),
583 "params": f.params,
584 "return_type": f.return_type,
585 "docstring": f.docstring,
586 "is_async": f.is_async,
587 "decorators": f.decorators,
588 })
589 })
590 .collect();
591
592 let call_graph = self
593 .call_graph
594 .as_ref()
595 .filter(|cg| !cg.calls.is_empty())
596 .map(|cg| {
597 json!({
598 "calls": cg.calls,
599 "called_by": cg.called_by,
600 })
601 })
602 .unwrap_or_else(|| json!({}));
603
604 json!({
605 "file_path": self.path,
606 "language": self.language,
607 "docstring": self.docstring,
608 "imports": imports,
609 "classes": classes,
610 "functions": functions,
611 "call_graph": call_graph,
612 })
613 }
614
615 #[allow(dead_code)]
622 pub fn to_compact(&self) -> Value {
623 self.to_compact_with_limits(200, 100)
624 }
625
626 #[allow(dead_code)]
630 pub fn to_compact_with_limits(
631 &self,
632 max_module_doc_len: usize,
633 max_class_doc_len: usize,
634 ) -> Value {
635 let truncate = |s: &str, max_len: usize| -> String {
636 if s.len() > max_len {
637 format!("{}...", &s[..max_len])
638 } else {
639 s.to_string()
640 }
641 };
642
643 let filename = Path::new(&self.path)
644 .file_name()
645 .map(|n| n.to_string_lossy().into_owned())
646 .unwrap_or_else(|| self.path.clone());
647
648 let mut result = json!({
649 "file": filename,
650 "lang": self.language,
651 });
652
653 if let Some(ref doc) = self.docstring {
654 result["doc"] = json!(truncate(doc, max_module_doc_len));
655 }
656
657 if !self.imports.is_empty() {
658 let import_stmts: Vec<String> = self.imports.iter().map(|i| i.statement()).collect();
659 result["imports"] = json!(import_stmts);
660 }
661
662 if !self.classes.is_empty() {
663 let mut classes_map = serde_json::Map::new();
664 for c in &self.classes {
665 let mut class_info = serde_json::Map::new();
666
667 if !c.bases.is_empty() {
668 class_info.insert("bases".to_string(), json!(c.bases));
669 }
670
671 if let Some(ref doc) = c.docstring {
672 class_info.insert("doc".to_string(), json!(truncate(doc, max_class_doc_len)));
673 }
674
675 if !c.methods.is_empty() {
676 let method_sigs: Vec<String> =
677 c.methods.iter().map(|m| m.signature()).collect();
678 class_info.insert("methods".to_string(), json!(method_sigs));
679 }
680
681 classes_map.insert(c.name.clone(), Value::Object(class_info));
682 }
683 result["classes"] = Value::Object(classes_map);
684 }
685
686 if !self.functions.is_empty() {
687 let func_sigs: Vec<String> = self.functions.iter().map(|f| f.signature()).collect();
688 result["functions"] = json!(func_sigs);
689 }
690
691 if let Some(ref cg) = self.call_graph {
692 if !cg.calls.is_empty() {
693 result["calls"] = json!(cg.calls);
694 }
695 }
696
697 result
698 }
699}
700
701#[derive(Debug, Clone, Serialize, Deserialize)]
716pub struct FileTreeEntry {
717 #[serde(default)]
719 pub name: String,
720 #[serde(rename = "type", default = "default_entry_type")]
722 pub entry_type: String,
723 #[serde(default)]
725 pub path: String,
726 #[serde(skip_serializing_if = "Vec::is_empty", default)]
728 pub children: Vec<FileTreeEntry>,
729 #[serde(skip_serializing_if = "std::ops::Not::not", default)]
732 pub depth_limited: bool,
733}
734
735impl Default for FileTreeEntry {
736 fn default() -> Self {
737 Self {
738 name: String::new(),
739 entry_type: default_entry_type(),
740 path: String::new(),
741 children: Vec::new(),
742 depth_limited: false,
743 }
744 }
745}
746
747impl FileTreeEntry {
748 #[inline]
750 pub fn is_dir(&self) -> bool {
751 self.entry_type == "dir"
752 }
753
754 pub fn new_dir(name: String, path: String, children: Vec<FileTreeEntry>) -> Self {
756 Self {
757 name,
758 entry_type: "dir".to_string(),
759 path,
760 children,
761 depth_limited: false,
762 }
763 }
764
765 pub fn new_file(name: String, path: String) -> Self {
767 Self {
768 name,
769 entry_type: "file".to_string(),
770 path,
771 children: vec![],
772 depth_limited: false,
773 }
774 }
775
776 pub fn depth_limit_reached(path: &std::path::Path, root: &std::path::Path) -> Self {
781 let name = path
785 .file_name()
786 .map(|n| n.to_string_lossy().into_owned())
787 .unwrap_or_else(|| ".".to_string());
788
789 let rel_path = if path == root {
790 ".".to_string()
791 } else {
792 path.strip_prefix(root)
793 .map(|p| p.display().to_string())
794 .unwrap_or_default()
795 };
796
797 Self {
798 name,
799 entry_type: "dir".to_string(),
800 path: rel_path,
801 children: vec![],
802 depth_limited: true,
803 }
804 }
805}
806
807#[derive(Debug, Clone, Default, Serialize, Deserialize)]
809pub struct CodeStructure {
810 #[serde(default)]
812 pub path: String,
813 #[serde(default)]
815 pub functions: Vec<FunctionSummary>,
816 #[serde(default)]
818 pub classes: Vec<ClassSummary>,
819 #[serde(default)]
822 pub files_processed: usize,
823 #[serde(default, skip_serializing_if = "is_zero")]
825 pub files_failed: usize,
826 #[serde(default, skip_serializing_if = "is_zero")]
828 pub files_skipped: usize,
829 #[serde(default)]
832 pub total_files: usize,
833}
834
835fn is_zero(n: &usize) -> bool {
837 *n == 0
838}
839
840#[derive(Debug, Clone, Default, Serialize, Deserialize)]
842pub struct FunctionSummary {
843 #[serde(default)]
844 pub name: String,
845 #[serde(default)]
846 pub file: String,
847 #[serde(default)]
848 pub line: usize,
849 #[serde(default)]
850 pub signature: String,
851}
852
853#[derive(Debug, Clone, Default, Serialize, Deserialize)]
855pub struct ClassSummary {
856 #[serde(default)]
857 pub name: String,
858 #[serde(default)]
859 pub file: String,
860 #[serde(default)]
861 pub line: usize,
862 #[serde(default)]
863 pub method_count: usize,
864}
865
866#[cfg(test)]
867mod tests {
868 use super::*;
869
870 #[test]
871 fn test_import_info_statement_from_import() {
872 let import = ImportInfo {
874 module: "os.path".to_string(),
875 names: vec!["join".to_string(), "dirname".to_string()],
876 aliases: HashMap::new(),
877 is_from: true,
878 level: 0,
879 line_number: 1,
880 visibility: None,
881 };
882 assert_eq!(import.statement(), "from os.path import join, dirname");
883 }
884
885 #[test]
886 fn test_import_info_statement_from_import_with_alias() {
887 let mut aliases = HashMap::new();
889 aliases.insert("dirname".to_string(), "d".to_string());
890
891 let import = ImportInfo {
892 module: "os.path".to_string(),
893 names: vec!["join".to_string(), "dirname".to_string()],
894 aliases,
895 is_from: true,
896 level: 0,
897 line_number: 1,
898 visibility: None,
899 };
900 assert_eq!(import.statement(), "from os.path import join, dirname as d");
901 }
902
903 #[test]
904 fn test_import_info_statement_relative_import() {
905 let import = ImportInfo {
907 module: "".to_string(),
908 names: vec!["utils".to_string()],
909 aliases: HashMap::new(),
910 is_from: true,
911 level: 2,
912 line_number: 1,
913 visibility: None,
914 };
915 assert_eq!(import.statement(), "from .. import utils");
916 }
917
918 #[test]
919 fn test_import_info_statement_relative_import_with_module() {
920 let import = ImportInfo {
922 module: "package".to_string(),
923 names: vec!["module".to_string()],
924 aliases: HashMap::new(),
925 is_from: true,
926 level: 3,
927 line_number: 1,
928 visibility: None,
929 };
930 assert_eq!(import.statement(), "from ...package import module");
931 }
932
933 #[test]
934 fn test_import_info_statement_simple_import() {
935 let import = ImportInfo {
937 module: "os".to_string(),
938 names: vec![],
939 aliases: HashMap::new(),
940 is_from: false,
941 level: 0,
942 line_number: 1,
943 visibility: None,
944 };
945 assert_eq!(import.statement(), "import os");
946 }
947
948 #[test]
949 fn test_import_info_statement_import_with_alias() {
950 let mut aliases = HashMap::new();
952 aliases.insert("numpy".to_string(), "np".to_string());
953
954 let import = ImportInfo {
955 module: "numpy".to_string(),
956 names: vec![],
957 aliases,
958 is_from: false,
959 level: 0,
960 line_number: 1,
961 visibility: None,
962 };
963 assert_eq!(import.statement(), "import numpy as np");
964 }
965
966 #[test]
967 fn test_import_info_statement_single_level_relative() {
968 let import = ImportInfo {
970 module: "".to_string(),
971 names: vec!["config".to_string()],
972 aliases: HashMap::new(),
973 is_from: true,
974 level: 1,
975 line_number: 1,
976 visibility: None,
977 };
978 assert_eq!(import.statement(), "from . import config");
979 }
980
981 #[test]
984 fn test_function_info_default() {
985 let func = FunctionInfo::default();
986 assert!(func.name.is_empty());
987 assert!(func.params.is_empty());
988 assert!(func.return_type.is_none());
989 assert!(func.docstring.is_none());
990 assert!(!func.is_method);
991 assert!(!func.is_async);
992 assert!(func.decorators.is_empty());
993 assert_eq!(func.line_number, 0);
994 assert!(func.end_line_number.is_none());
995 assert_eq!(func.language, "python"); }
997
998 #[test]
999 fn test_class_info_default() {
1000 let class = ClassInfo::default();
1001 assert!(class.name.is_empty());
1002 assert!(class.bases.is_empty());
1003 assert!(class.docstring.is_none());
1004 assert!(class.methods.is_empty());
1005 assert!(class.fields.is_empty());
1006 assert!(class.inner_classes.is_empty());
1007 assert!(class.decorators.is_empty());
1008 assert_eq!(class.line_number, 0);
1009 assert!(class.end_line_number.is_none());
1010 assert_eq!(class.language, "python"); }
1012
1013 #[test]
1014 fn test_import_info_default() {
1015 let import = ImportInfo::default();
1016 assert!(import.module.is_empty());
1017 assert!(import.names.is_empty());
1018 assert!(import.aliases.is_empty());
1019 assert!(!import.is_from);
1020 assert_eq!(import.level, 0);
1021 assert_eq!(import.line_number, 0);
1022 assert!(import.visibility.is_none());
1023 }
1024
1025 #[test]
1026 fn test_module_info_default() {
1027 let module = ModuleInfo::default();
1028 assert!(module.path.is_empty());
1029 assert_eq!(module.language, "python"); assert!(module.docstring.is_none());
1031 assert!(module.functions.is_empty());
1032 assert!(module.classes.is_empty());
1033 assert!(module.imports.is_empty());
1034 assert!(module.call_graph.is_none());
1035 }
1036
1037 #[test]
1038 fn test_field_info_default() {
1039 let field = FieldInfo::default();
1040 assert!(field.name.is_empty());
1041 assert!(field.field_type.is_none());
1042 assert!(field.visibility.is_none());
1043 assert!(!field.is_static);
1044 assert!(!field.is_final);
1045 assert!(field.default_value.is_none());
1046 assert!(field.annotations.is_empty());
1047 assert_eq!(field.line_number, 0);
1048 }
1049
1050 #[test]
1051 fn test_file_tree_entry_default() {
1052 let entry = FileTreeEntry::default();
1053 assert!(entry.name.is_empty());
1054 assert_eq!(entry.entry_type, "file"); assert!(entry.path.is_empty());
1056 assert!(entry.children.is_empty());
1057 assert!(!entry.depth_limited);
1058 }
1059
1060 #[test]
1061 fn test_code_structure_default() {
1062 let structure = CodeStructure::default();
1063 assert!(structure.path.is_empty());
1064 assert!(structure.functions.is_empty());
1065 assert!(structure.classes.is_empty());
1066 assert_eq!(structure.files_processed, 0);
1067 assert_eq!(structure.files_failed, 0);
1068 assert_eq!(structure.files_skipped, 0);
1069 assert_eq!(structure.total_files, 0);
1070 }
1071
1072 #[test]
1073 fn test_function_summary_default() {
1074 let summary = FunctionSummary::default();
1075 assert!(summary.name.is_empty());
1076 assert!(summary.file.is_empty());
1077 assert_eq!(summary.line, 0);
1078 assert!(summary.signature.is_empty());
1079 }
1080
1081 #[test]
1082 fn test_class_summary_default() {
1083 let summary = ClassSummary::default();
1084 assert!(summary.name.is_empty());
1085 assert!(summary.file.is_empty());
1086 assert_eq!(summary.line, 0);
1087 assert_eq!(summary.method_count, 0);
1088 }
1089
1090 #[test]
1091 fn test_deserialize_with_missing_fields() {
1092 let json = r#"{"name": "test_func"}"#;
1094 let func: FunctionInfo = serde_json::from_str(json).unwrap();
1095 assert_eq!(func.name, "test_func");
1096 assert_eq!(func.language, "python"); assert!(func.params.is_empty());
1098 }
1099
1100 #[test]
1101 fn test_deserialize_class_with_missing_fields() {
1102 let json = r#"{"name": "TestClass", "line_number": 10}"#;
1103 let class: ClassInfo = serde_json::from_str(json).unwrap();
1104 assert_eq!(class.name, "TestClass");
1105 assert_eq!(class.line_number, 10);
1106 assert_eq!(class.language, "python"); assert!(class.methods.is_empty());
1108 }
1109
1110 #[test]
1113 fn test_module_info_to_dict() {
1114 let module = ModuleInfo {
1115 path: "/src/test.py".to_string(),
1116 language: "python".to_string(),
1117 docstring: Some("Test module".to_string()),
1118 functions: vec![FunctionInfo {
1119 name: "test_func".to_string(),
1120 params: vec!["x: int".to_string()],
1121 return_type: Some("str".to_string()),
1122 line_number: 10,
1123 language: "python".to_string(),
1124 ..Default::default()
1125 }],
1126 classes: vec![],
1127 imports: vec![],
1128 call_graph: None,
1129 };
1130
1131 let dict = module.to_dict();
1132 assert_eq!(dict["file_path"], "/src/test.py");
1133 assert_eq!(dict["language"], "python");
1134 assert_eq!(dict["docstring"], "Test module");
1135 assert_eq!(dict["functions"][0]["name"], "test_func");
1136 assert_eq!(
1137 dict["functions"][0]["signature"],
1138 "def test_func(x: int) -> str"
1139 );
1140 }
1141
1142 #[test]
1143 fn test_module_info_to_dict_with_classes() {
1144 let module = ModuleInfo {
1145 path: "/src/api.py".to_string(),
1146 language: "python".to_string(),
1147 docstring: None,
1148 functions: vec![],
1149 classes: vec![ClassInfo {
1150 name: "UserController".to_string(),
1151 bases: vec!["BaseController".to_string()],
1152 docstring: Some("User API controller".to_string()),
1153 methods: vec![FunctionInfo {
1154 name: "get_user".to_string(),
1155 params: vec!["self".to_string(), "user_id: int".to_string()],
1156 return_type: Some("User".to_string()),
1157 is_method: true,
1158 language: "python".to_string(),
1159 ..Default::default()
1160 }],
1161 language: "python".to_string(),
1162 ..Default::default()
1163 }],
1164 imports: vec![ImportInfo {
1165 module: "flask".to_string(),
1166 names: vec!["Flask".to_string(), "request".to_string()],
1167 is_from: true,
1168 ..Default::default()
1169 }],
1170 call_graph: None,
1171 };
1172
1173 let dict = module.to_dict();
1174 assert_eq!(dict["classes"][0]["name"], "UserController");
1175 assert_eq!(
1176 dict["classes"][0]["signature"],
1177 "class UserController(BaseController)"
1178 );
1179 assert_eq!(dict["classes"][0]["methods"][0]["name"], "get_user");
1180 assert_eq!(
1181 dict["classes"][0]["methods"][0]["signature"],
1182 "def get_user(self, user_id: int) -> User"
1183 );
1184 assert_eq!(dict["imports"][0]["module"], "flask");
1185 }
1186
1187 #[test]
1188 fn test_module_info_to_compact() {
1189 let module = ModuleInfo {
1190 path: "/src/test.py".to_string(),
1191 language: "python".to_string(),
1192 docstring: Some("A".repeat(300)), functions: vec![FunctionInfo {
1194 name: "test_func".to_string(),
1195 params: vec!["x: int".to_string()],
1196 return_type: Some("str".to_string()),
1197 language: "python".to_string(),
1198 ..Default::default()
1199 }],
1200 classes: vec![ClassInfo {
1201 name: "TestClass".to_string(),
1202 bases: vec!["Base".to_string()],
1203 docstring: Some("B".repeat(200)), methods: vec![FunctionInfo {
1205 name: "method".to_string(),
1206 params: vec!["self".to_string()],
1207 language: "python".to_string(),
1208 ..Default::default()
1209 }],
1210 language: "python".to_string(),
1211 ..Default::default()
1212 }],
1213 imports: vec![ImportInfo {
1214 module: "os".to_string(),
1215 is_from: false,
1216 ..Default::default()
1217 }],
1218 call_graph: None,
1219 };
1220
1221 let compact = module.to_compact();
1222 assert_eq!(compact["file"], "test.py");
1223 assert_eq!(compact["lang"], "python");
1224 let doc = compact["doc"].as_str().unwrap();
1226 assert!(doc.len() <= 203);
1227 assert!(doc.ends_with("..."));
1228 assert_eq!(compact["imports"][0], "import os");
1229 assert_eq!(compact["functions"][0], "def test_func(x: int) -> str");
1230 let class_doc = compact["classes"]["TestClass"]["doc"].as_str().unwrap();
1232 assert!(class_doc.len() <= 103);
1233 assert!(class_doc.ends_with("..."));
1234 assert_eq!(compact["classes"]["TestClass"]["bases"][0], "Base");
1235 assert_eq!(
1236 compact["classes"]["TestClass"]["methods"][0],
1237 "def method(self)"
1238 );
1239 }
1240
1241 #[test]
1242 fn test_module_info_to_compact_with_call_graph() {
1243 let mut cg = CallGraphInfo::new();
1244 cg.add_call("main", "process_data");
1245 cg.add_call("main", "cleanup");
1246
1247 let module = ModuleInfo {
1248 path: "/src/main.py".to_string(),
1249 language: "python".to_string(),
1250 docstring: None,
1251 functions: vec![
1252 FunctionInfo {
1253 name: "main".to_string(),
1254 language: "python".to_string(),
1255 ..Default::default()
1256 },
1257 FunctionInfo {
1258 name: "process_data".to_string(),
1259 language: "python".to_string(),
1260 ..Default::default()
1261 },
1262 ],
1263 classes: vec![],
1264 imports: vec![],
1265 call_graph: Some(cg),
1266 };
1267
1268 let compact = module.to_compact();
1269 assert!(compact["calls"]["main"].as_array().is_some());
1270 let calls = compact["calls"]["main"].as_array().unwrap();
1271 assert!(calls.iter().any(|v| v == "process_data"));
1272 assert!(calls.iter().any(|v| v == "cleanup"));
1273 }
1274
1275 #[test]
1276 fn test_module_info_to_compact_custom_limits() {
1277 let module = ModuleInfo {
1278 path: "/src/test.py".to_string(),
1279 language: "python".to_string(),
1280 docstring: Some("A".repeat(100)), functions: vec![],
1282 classes: vec![ClassInfo {
1283 name: "TestClass".to_string(),
1284 docstring: Some("B".repeat(50)), language: "python".to_string(),
1286 ..Default::default()
1287 }],
1288 imports: vec![],
1289 call_graph: None,
1290 };
1291
1292 let compact = module.to_compact();
1294 assert!(!compact["doc"].as_str().unwrap().ends_with("..."));
1295 assert!(
1296 !compact["classes"]["TestClass"]["doc"]
1297 .as_str()
1298 .unwrap()
1299 .ends_with("...")
1300 );
1301
1302 let compact_short = module.to_compact_with_limits(50, 25);
1304 assert!(compact_short["doc"].as_str().unwrap().ends_with("..."));
1305 assert!(compact_short["classes"]["TestClass"]["doc"]
1306 .as_str()
1307 .unwrap()
1308 .ends_with("..."));
1309 }
1310}