go_brrr/ast/
types.rs

1//! AST type definitions.
2//!
3//! Core data structures for representing extracted code elements.
4
5use serde::{Deserialize, Serialize};
6use serde_json::{json, Value};
7use std::collections::HashMap;
8use std::path::Path;
9
10/// Default language for AST types (matches Python implementation).
11fn default_language() -> String {
12    "python".to_string()
13}
14
15/// Default entry type for file tree entries.
16fn default_entry_type() -> String {
17    "file".to_string()
18}
19
20/// Information about a function or method.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct FunctionInfo {
23    /// Function name
24    #[serde(default)]
25    pub name: String,
26    /// Parameter names (with optional type annotations)
27    #[serde(default)]
28    pub params: Vec<String>,
29    /// Return type annotation if present
30    #[serde(default)]
31    pub return_type: Option<String>,
32    /// Docstring or doc comment
33    #[serde(default)]
34    pub docstring: Option<String>,
35    /// Whether this is a method (has self/this)
36    #[serde(default)]
37    pub is_method: bool,
38    /// Whether this is an async function
39    #[serde(default)]
40    pub is_async: bool,
41    /// Decorators/attributes applied
42    #[serde(default)]
43    pub decorators: Vec<String>,
44    /// Starting line number (1-indexed)
45    #[serde(default)]
46    pub line_number: usize,
47    /// Ending line number (1-indexed)
48    #[serde(default)]
49    pub end_line_number: Option<usize>,
50    /// Source language
51    #[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    /// Generate a language-appropriate signature string.
74    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            // Default to Python-style for unknown languages
150            _ => {
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/// Information about a field or member variable.
163#[derive(Debug, Clone, Default, Serialize, Deserialize)]
164pub struct FieldInfo {
165    /// Field name
166    #[serde(default)]
167    pub name: String,
168    /// Field type
169    #[serde(default)]
170    pub field_type: Option<String>,
171    /// Visibility modifier (public/private/protected)
172    #[serde(default)]
173    pub visibility: Option<String>,
174    /// Whether field is static
175    #[serde(default)]
176    pub is_static: bool,
177    /// Whether field is final/const
178    #[serde(default)]
179    pub is_final: bool,
180    /// Initial value expression (if any)
181    #[serde(default)]
182    pub default_value: Option<String>,
183    /// Annotations/attributes
184    #[serde(default)]
185    pub annotations: Vec<String>,
186    /// Starting line number (1-indexed)
187    #[serde(default)]
188    pub line_number: usize,
189}
190
191/// Information about a class or struct.
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct ClassInfo {
194    /// Class name
195    #[serde(default)]
196    pub name: String,
197    /// Base classes / implemented interfaces
198    #[serde(default)]
199    pub bases: Vec<String>,
200    /// Docstring or doc comment
201    #[serde(default)]
202    pub docstring: Option<String>,
203    /// Methods defined in this class
204    #[serde(default)]
205    pub methods: Vec<FunctionInfo>,
206    /// Fields/member variables
207    #[serde(default, skip_serializing_if = "Vec::is_empty")]
208    pub fields: Vec<FieldInfo>,
209    /// BUG #7 FIX: Inner/nested classes
210    #[serde(default, skip_serializing_if = "Vec::is_empty")]
211    pub inner_classes: Vec<ClassInfo>,
212    /// Decorators/attributes applied
213    #[serde(default)]
214    pub decorators: Vec<String>,
215    /// Starting line number (1-indexed)
216    #[serde(default)]
217    pub line_number: usize,
218    /// Ending line number (1-indexed)
219    #[serde(default)]
220    pub end_line_number: Option<usize>,
221    /// Source language
222    #[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    /// Generate a language-appropriate class signature string.
245    ///
246    /// Produces idiomatic class declarations for each supported language:
247    /// - Python: `class Foo(Base1, Base2)`
248    /// - TypeScript/JavaScript: `class Foo extends Base`
249    /// - Go: `type Foo struct`
250    /// - Rust: `struct Foo`
251    /// - Java/Kotlin/C#: `class Foo extends Base`
252    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                // Python default
274                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/// Information about an import statement.
285#[derive(Debug, Clone, Default, Serialize, Deserialize)]
286pub struct ImportInfo {
287    /// Module being imported
288    #[serde(default)]
289    pub module: String,
290    /// Specific names imported (empty for `import module`)
291    #[serde(default)]
292    pub names: Vec<String>,
293    /// Aliases (original_name -> alias)
294    #[serde(default)]
295    pub aliases: HashMap<String, String>,
296    /// Whether this is a `from X import Y` style
297    #[serde(default)]
298    pub is_from: bool,
299    /// Relative import level (0 for absolute)
300    #[serde(default)]
301    pub level: usize,
302    /// Starting line number (1-indexed)
303    #[serde(default)]
304    pub line_number: usize,
305    /// Visibility modifier (e.g., "pub", "pub(crate)" for Rust re-exports).
306    /// None for private imports, Some(...) for public re-exports.
307    #[serde(default, skip_serializing_if = "Option::is_none")]
308    pub visibility: Option<String>,
309}
310
311impl ImportInfo {
312    /// Reconstruct the original import statement from parsed components.
313    ///
314    /// Generates idiomatic import syntax based on the `is_from` flag:
315    /// - `from X import Y` style: `from {level_dots}{module} import {names}`
316    /// - `import X` style: `import {module} [as alias]`
317    ///
318    /// Handles relative imports (level > 0 adds leading dots) and aliases.
319    ///
320    /// # Examples
321    ///
322    /// ```
323    /// use std::collections::HashMap;
324    /// use go_brrr::ast::types::ImportInfo;
325    ///
326    /// // from os.path import join, dirname as d
327    /// let import = ImportInfo {
328    ///     module: "os.path".to_string(),
329    ///     names: vec!["join".to_string(), "dirname".to_string()],
330    ///     aliases: [("dirname".to_string(), "d".to_string())].into_iter().collect(),
331    ///     is_from: true,
332    ///     level: 0,
333    ///     line_number: 1,
334    ///     visibility: None,
335    /// };
336    /// assert_eq!(import.statement(), "from os.path import join, dirname as d");
337    ///
338    /// // from .. import utils
339    /// let relative = ImportInfo {
340    ///     module: "".to_string(),
341    ///     names: vec!["utils".to_string()],
342    ///     aliases: HashMap::new(),
343    ///     is_from: true,
344    ///     level: 2,
345    ///     line_number: 2,
346    ///     visibility: None,
347    /// };
348    /// assert_eq!(relative.statement(), "from .. import utils");
349    ///
350    /// // import numpy as np
351    /// let aliased = ImportInfo {
352    ///     module: "numpy".to_string(),
353    ///     names: vec![],
354    ///     aliases: [("numpy".to_string(), "np".to_string())].into_iter().collect(),
355    ///     is_from: false,
356    ///     level: 0,
357    ///     line_number: 3,
358    ///     visibility: None,
359    /// };
360    /// assert_eq!(aliased.statement(), "import numpy as np");
361    /// ```
362    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            // Regular import: import module [as alias]
389            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/// Module-level call graph information.
399///
400/// Tracks which functions in this module call which other functions.
401/// This is a lightweight, per-module representation of call relationships,
402/// distinct from the project-wide `CallGraph` in the callgraph module.
403#[derive(Debug, Clone, Default, Serialize, Deserialize)]
404pub struct CallGraphInfo {
405    /// Map of caller function name to list of called function names.
406    /// Key is the caller (e.g., "process_data" or "MyClass.method"),
407    /// value is a list of callees (function names being called).
408    pub calls: HashMap<String, Vec<String>>,
409
410    /// Reverse mapping: function name to list of functions that call it.
411    /// Key is the callee, value is list of callers.
412    /// This enables efficient reverse lookups for impact analysis.
413    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
414    pub called_by: HashMap<String, Vec<String>>,
415}
416
417impl CallGraphInfo {
418    /// Create a new empty call graph.
419    ///
420    /// Part of public API for library consumers building call graphs programmatically.
421    #[allow(dead_code)]
422    pub fn new() -> Self {
423        Self::default()
424    }
425
426    /// Record a function call, updating both forward and reverse mappings.
427    ///
428    /// Part of public API for library consumers building call graphs programmatically.
429    ///
430    /// # Arguments
431    /// * `caller` - The name of the calling function
432    /// * `callee` - The name of the called function
433    #[allow(dead_code)]
434    pub fn add_call(&mut self, caller: &str, callee: &str) {
435        // Update forward mapping: caller -> callees
436        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        // Update reverse mapping: callee -> callers
442        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    /// Check if the call graph is empty.
449    ///
450    /// Part of public API for library consumers querying call graphs.
451    #[allow(dead_code)]
452    pub fn is_empty(&self) -> bool {
453        self.calls.is_empty()
454    }
455
456    /// Get all functions called by the given function.
457    ///
458    /// Part of public API for library consumers querying call graphs.
459    #[allow(dead_code)]
460    pub fn get_callees(&self, caller: &str) -> Option<&Vec<String>> {
461        self.calls.get(caller)
462    }
463
464    /// Get all functions that call the given function.
465    ///
466    /// Part of public API for library consumers querying call graphs.
467    #[allow(dead_code)]
468    pub fn get_callers(&self, callee: &str) -> Option<&Vec<String>> {
469        self.called_by.get(callee)
470    }
471}
472
473/// Information about a complete module/file.
474#[derive(Debug, Clone, Serialize, Deserialize)]
475pub struct ModuleInfo {
476    /// File path (serializes as "file_path" for Python compatibility)
477    #[serde(rename = "file_path", default)]
478    pub path: String,
479    /// Detected language
480    #[serde(default = "default_language")]
481    pub language: String,
482    /// Module-level docstring (e.g., Python module docstrings)
483    #[serde(default, skip_serializing_if = "Option::is_none")]
484    pub docstring: Option<String>,
485    /// Top-level functions
486    #[serde(default)]
487    pub functions: Vec<FunctionInfo>,
488    /// Classes/structs
489    #[serde(default)]
490    pub classes: Vec<ClassInfo>,
491    /// Import statements
492    #[serde(default)]
493    pub imports: Vec<ImportInfo>,
494    /// Module-level call graph (function relationships within this module).
495    /// None if not computed (call graph extraction is optional/expensive).
496    #[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    /// Convert to dictionary format with computed signatures.
516    ///
517    /// Returns a complete JSON representation suitable for serialization,
518    /// with all function and class signatures pre-computed. This format
519    /// matches the Python `ModuleInfo.to_dict()` output for compatibility.
520    ///
521    /// Serialization helper for library consumers needing full module representation.
522    #[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    /// Convert to compact format optimized for LLM context.
616    ///
617    /// Returns a condensed JSON representation that minimizes token usage
618    /// while preserving essential information.
619    ///
620    /// Serialization helper for library consumers needing compact representation.
621    #[allow(dead_code)]
622    pub fn to_compact(&self) -> Value {
623        self.to_compact_with_limits(200, 100)
624    }
625
626    /// Convert to compact format with custom docstring length limits.
627    ///
628    /// Serialization helper for library consumers needing compact representation.
629    #[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/// File tree entry.
702///
703/// JSON schema matches Python implementation:
704/// ```json
705/// {
706///   "name": "project",
707///   "type": "dir",
708///   "path": ".",
709///   "children": [
710///     {"name": "src", "type": "dir", "path": "src", "children": [...]},
711///     {"name": "main.py", "type": "file", "path": "src/main.py"}
712///   ]
713/// }
714/// ```
715#[derive(Debug, Clone, Serialize, Deserialize)]
716pub struct FileTreeEntry {
717    /// File or directory name
718    #[serde(default)]
719    pub name: String,
720    /// Entry type: "dir" or "file" (matches Python schema)
721    #[serde(rename = "type", default = "default_entry_type")]
722    pub entry_type: String,
723    /// Relative path from project root
724    #[serde(default)]
725    pub path: String,
726    /// Children (for directories)
727    #[serde(skip_serializing_if = "Vec::is_empty", default)]
728    pub children: Vec<FileTreeEntry>,
729    /// Indicates this directory was not expanded due to depth limit.
730    /// When true, children may be incomplete or empty because max_depth was reached.
731    #[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    /// Check if this entry is a directory.
749    #[inline]
750    pub fn is_dir(&self) -> bool {
751        self.entry_type == "dir"
752    }
753
754    /// Create a new directory entry.
755    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    /// Create a new file entry.
766    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    /// Create a placeholder entry for a directory that hit the depth limit.
777    ///
778    /// This marker indicates the directory exists but its contents were not
779    /// traversed to prevent stack overflow from deeply nested structures.
780    pub fn depth_limit_reached(path: &std::path::Path, root: &std::path::Path) -> Self {
781        // BUG-012 FIX: Use to_string_lossy() to preserve as much information as possible
782        // for non-UTF8 paths instead of silently converting to ".".
783        // The replacement character U+FFFD indicates lossy conversion occurred.
784        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/// Code structure summary.
808#[derive(Debug, Clone, Default, Serialize, Deserialize)]
809pub struct CodeStructure {
810    /// Path analyzed
811    #[serde(default)]
812    pub path: String,
813    /// Functions found
814    #[serde(default)]
815    pub functions: Vec<FunctionSummary>,
816    /// Classes found
817    #[serde(default)]
818    pub classes: Vec<ClassSummary>,
819    /// Number of files successfully parsed and analyzed.
820    /// This is the primary metric - use this to know how many files contributed data.
821    #[serde(default)]
822    pub files_processed: usize,
823    /// Number of files that failed AST extraction (parse errors, encoding issues, etc.).
824    #[serde(default, skip_serializing_if = "is_zero")]
825    pub files_failed: usize,
826    /// Number of files skipped due to early termination (max_results limit) or security.
827    #[serde(default, skip_serializing_if = "is_zero")]
828    pub files_skipped: usize,
829    /// Total source files found matching the language filter.
830    /// Equals: files_processed + files_failed + files_skipped.
831    #[serde(default)]
832    pub total_files: usize,
833}
834
835/// Helper for serde skip_serializing_if to omit zero values from JSON output.
836fn is_zero(n: &usize) -> bool {
837    *n == 0
838}
839
840/// Brief function summary for structure output.
841#[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/// Brief class summary for structure output.
854#[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        // from os.path import join, dirname
873        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        // from os.path import join, dirname as d
888        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        // from .. import utils
906        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        // from ...package import module
921        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        // import os
936        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        // import numpy as np
951        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        // from . import config
969        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    // Tests for Default trait implementations (AST-BUG-10 fix)
982
983    #[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"); // Custom default
996    }
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"); // Custom default
1011    }
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"); // Custom default
1030        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"); // Custom default
1055        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        // Verify serde deserialization works with missing fields
1093        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"); // Uses custom default
1097        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"); // Uses custom default
1107        assert!(class.methods.is_empty());
1108    }
1109
1110    // Tests for to_dict() and to_compact() methods (AST-BUG-11 & BUG-12 fix)
1111
1112    #[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)), // Long docstring
1193            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)), // Long docstring
1204                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        // Module docstring truncated to 200 chars + "..."
1225        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        // Class docstring truncated to 100 chars + "..."
1231        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)), // 100 chars
1281            functions: vec![],
1282            classes: vec![ClassInfo {
1283                name: "TestClass".to_string(),
1284                docstring: Some("B".repeat(50)), // 50 chars
1285                language: "python".to_string(),
1286                ..Default::default()
1287            }],
1288            imports: vec![],
1289            call_graph: None,
1290        };
1291
1292        // With default limits (200, 100), nothing truncated
1293        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        // With custom limits (50, 25), both truncated
1303        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}