Skip to main content

tldr_cli/commands/remaining/
types.rs

1//! Shared types for remaining commands
2//!
3//! This module defines all data types used across the remaining analysis
4//! commands. Types are designed for JSON serialization with serde.
5
6use std::collections::HashMap;
7
8use clap::ValueEnum;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12// =============================================================================
13// Output Format
14// =============================================================================
15
16/// Output format for all commands.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum OutputFormat {
20    /// JSON output (default)
21    #[default]
22    Json,
23    /// Human-readable text output
24    Text,
25    /// SARIF format (only for vuln command)
26    Sarif,
27}
28
29impl std::fmt::Display for OutputFormat {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        match self {
32            Self::Json => write!(f, "json"),
33            Self::Text => write!(f, "text"),
34            Self::Sarif => write!(f, "sarif"),
35        }
36    }
37}
38
39// =============================================================================
40// Severity Level
41// =============================================================================
42
43/// Severity level for findings.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ValueEnum, Default)]
45#[serde(rename_all = "lowercase")]
46pub enum Severity {
47    Critical,
48    High,
49    #[default]
50    Medium,
51    Low,
52    Info,
53}
54
55impl Severity {
56    /// Returns ordering value (lower = more severe)
57    pub fn order(&self) -> u8 {
58        match self {
59            Self::Critical => 0,
60            Self::High => 1,
61            Self::Medium => 2,
62            Self::Low => 3,
63            Self::Info => 4,
64        }
65    }
66}
67
68impl std::fmt::Display for Severity {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        match self {
71            Self::Critical => write!(f, "critical"),
72            Self::High => write!(f, "high"),
73            Self::Medium => write!(f, "medium"),
74            Self::Low => write!(f, "low"),
75            Self::Info => write!(f, "info"),
76        }
77    }
78}
79
80// =============================================================================
81// Location
82// =============================================================================
83
84/// A location in source code (shared across multiple commands).
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct Location {
87    pub file: String,
88    pub line: u32,
89    #[serde(default)]
90    pub column: u32,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub end_line: Option<u32>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub end_column: Option<u32>,
95}
96
97impl Location {
98    /// Create a new location
99    pub fn new(file: impl Into<String>, line: u32) -> Self {
100        Self {
101            file: file.into(),
102            line,
103            column: 0,
104            end_line: None,
105            end_column: None,
106        }
107    }
108
109    /// Create a location with column information
110    pub fn with_column(file: impl Into<String>, line: u32, column: u32) -> Self {
111        Self {
112            file: file.into(),
113            line,
114            column,
115            end_line: None,
116            end_column: None,
117        }
118    }
119}
120
121// =============================================================================
122// Todo Types
123// =============================================================================
124
125/// A single improvement item.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct TodoItem {
128    /// Category of improvement (e.g., "dead_code", "complexity", "cohesion")
129    pub category: String,
130    /// Priority (lower = higher priority)
131    pub priority: u32,
132    /// Human-readable description
133    pub description: String,
134    /// File where the issue was found
135    #[serde(default)]
136    pub file: String,
137    /// Line number
138    #[serde(default)]
139    pub line: u32,
140    /// Severity level
141    #[serde(default)]
142    pub severity: String,
143    /// Score from sub-analysis (0.0-1.0 typically)
144    #[serde(default)]
145    pub score: f64,
146}
147
148impl TodoItem {
149    /// Create a new TodoItem
150    pub fn new(category: impl Into<String>, priority: u32, description: impl Into<String>) -> Self {
151        Self {
152            category: category.into(),
153            priority,
154            description: description.into(),
155            file: String::new(),
156            line: 0,
157            severity: String::new(),
158            score: 0.0,
159        }
160    }
161
162    /// Set file and line
163    pub fn with_location(mut self, file: impl Into<String>, line: u32) -> Self {
164        self.file = file.into();
165        self.line = line;
166        self
167    }
168
169    /// Set severity
170    pub fn with_severity(mut self, severity: impl Into<String>) -> Self {
171        self.severity = severity.into();
172        self
173    }
174
175    /// Set score
176    pub fn with_score(mut self, score: f64) -> Self {
177        self.score = score;
178        self
179    }
180}
181
182/// Summary of todo analysis.
183#[derive(Debug, Clone, Default, Serialize, Deserialize)]
184pub struct TodoSummary {
185    /// Number of dead code items found
186    pub dead_count: u32,
187    /// Number of similar code pairs found
188    pub similar_pairs: u32,
189    /// Number of classes with low cohesion
190    pub low_cohesion_count: u32,
191    /// Number of complexity hotspots
192    pub hotspot_count: u32,
193    /// Number of equivalence groups (redundant expressions)
194    pub equivalence_groups: u32,
195}
196
197/// Todo analysis report.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct TodoReport {
200    /// Command identifier
201    pub wrapper: String,
202    /// Path that was analyzed
203    pub path: String,
204    /// Improvement items sorted by priority
205    pub items: Vec<TodoItem>,
206    /// Summary statistics
207    pub summary: TodoSummary,
208    /// Raw results from sub-analyses (when --detail is used)
209    #[serde(default)]
210    pub sub_results: HashMap<String, Value>,
211    /// Total elapsed time in milliseconds
212    pub total_elapsed_ms: f64,
213}
214
215impl TodoReport {
216    /// Create a new TodoReport
217    pub fn new(path: impl Into<String>) -> Self {
218        Self {
219            wrapper: "todo".to_string(),
220            path: path.into(),
221            items: Vec::new(),
222            summary: TodoSummary::default(),
223            sub_results: HashMap::new(),
224            total_elapsed_ms: 0.0,
225        }
226    }
227}
228
229// =============================================================================
230// Secure Types
231// =============================================================================
232
233/// A security finding from the secure command.
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct SecureFinding {
236    /// Category of finding (e.g., "taint", "resource_leak", "bounds")
237    pub category: String,
238    /// Severity level
239    pub severity: String,
240    /// Human-readable description
241    pub description: String,
242    /// File where the issue was found
243    #[serde(default)]
244    pub file: String,
245    /// Line number
246    #[serde(default)]
247    pub line: u32,
248}
249
250impl SecureFinding {
251    /// Create a new SecureFinding
252    pub fn new(
253        category: impl Into<String>,
254        severity: impl Into<String>,
255        description: impl Into<String>,
256    ) -> Self {
257        Self {
258            category: category.into(),
259            severity: severity.into(),
260            description: description.into(),
261            file: String::new(),
262            line: 0,
263        }
264    }
265
266    /// Set file and line
267    pub fn with_location(mut self, file: impl Into<String>, line: u32) -> Self {
268        self.file = file.into();
269        self.line = line;
270        self
271    }
272}
273
274/// Security summary.
275#[derive(Debug, Clone, Default, Serialize, Deserialize)]
276pub struct SecureSummary {
277    /// Number of taint-related findings
278    pub taint_count: u32,
279    /// Number of critical taint findings
280    pub taint_critical: u32,
281    /// Number of resource leak findings
282    pub leak_count: u32,
283    /// Number of bounds/overflow warnings
284    pub bounds_warnings: u32,
285    /// Number of missing contracts
286    pub missing_contracts: u32,
287    /// Number of mutable parameter issues
288    pub mutable_params: u32,
289    /// Number of Rust unsafe blocks
290    #[serde(default)]
291    pub unsafe_blocks: u32,
292    /// Number of Rust raw pointer operations
293    #[serde(default)]
294    pub raw_pointer_ops: u32,
295    /// Number of Rust unwrap calls
296    #[serde(default)]
297    pub unwrap_calls: u32,
298    /// Number of todo!/unimplemented! markers in non-test code
299    #[serde(default)]
300    pub todo_markers: u32,
301}
302
303/// Secure analysis report.
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct SecureReport {
306    /// Command identifier
307    pub wrapper: String,
308    /// Path that was analyzed
309    pub path: String,
310    /// Security findings sorted by severity
311    pub findings: Vec<SecureFinding>,
312    /// Summary statistics
313    pub summary: SecureSummary,
314    /// Raw results from sub-analyses (when --detail is used)
315    #[serde(default)]
316    pub sub_results: HashMap<String, Value>,
317    /// Total elapsed time in milliseconds
318    pub total_elapsed_ms: f64,
319}
320
321impl SecureReport {
322    /// Create a new SecureReport
323    pub fn new(path: impl Into<String>) -> Self {
324        Self {
325            wrapper: "secure".to_string(),
326            path: path.into(),
327            findings: Vec::new(),
328            summary: SecureSummary::default(),
329            sub_results: HashMap::new(),
330            total_elapsed_ms: 0.0,
331        }
332    }
333}
334
335// =============================================================================
336// Tests
337// =============================================================================
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn test_output_format_serialization() {
345        let json = serde_json::to_string(&OutputFormat::Json).unwrap();
346        assert_eq!(json, r#""json""#);
347
348        let text = serde_json::to_string(&OutputFormat::Text).unwrap();
349        assert_eq!(text, r#""text""#);
350
351        let sarif = serde_json::to_string(&OutputFormat::Sarif).unwrap();
352        assert_eq!(sarif, r#""sarif""#);
353    }
354
355    #[test]
356    fn test_severity_ordering() {
357        assert!(Severity::Critical.order() < Severity::High.order());
358        assert!(Severity::High.order() < Severity::Medium.order());
359        assert!(Severity::Medium.order() < Severity::Low.order());
360        assert!(Severity::Low.order() < Severity::Info.order());
361    }
362
363    #[test]
364    fn test_location_serialization() {
365        let loc = Location::new("test.py", 42);
366        let json = serde_json::to_string(&loc).unwrap();
367        assert!(json.contains(r#""file":"test.py""#));
368        assert!(json.contains(r#""line":42"#));
369    }
370
371    #[test]
372    fn test_todo_report_serialization() {
373        let mut report = TodoReport::new("/path/to/file.py");
374        report
375            .items
376            .push(TodoItem::new("dead_code", 1, "Unused function"));
377        report.summary.dead_count = 1;
378
379        let json = serde_json::to_string(&report).unwrap();
380        assert!(json.contains(r#""wrapper":"todo""#));
381        assert!(json.contains(r#""dead_count":1"#));
382    }
383
384    #[test]
385    fn test_todo_item_builder() {
386        let item = TodoItem::new("complexity", 2, "High cyclomatic complexity")
387            .with_location("src/main.py", 100)
388            .with_severity("high")
389            .with_score(0.85);
390
391        assert_eq!(item.category, "complexity");
392        assert_eq!(item.file, "src/main.py");
393        assert_eq!(item.line, 100);
394        assert_eq!(item.severity, "high");
395        assert!((item.score - 0.85).abs() < 0.001);
396    }
397
398    #[test]
399    fn test_secure_report_serialization() {
400        let mut report = SecureReport::new("/path/to/file.py");
401        report
402            .findings
403            .push(SecureFinding::new("taint", "critical", "SQL injection"));
404        report.summary.taint_count = 1;
405        report.summary.taint_critical = 1;
406
407        let json = serde_json::to_string(&report).unwrap();
408        assert!(json.contains(r#""wrapper":"secure""#));
409        assert!(json.contains(r#""taint_count":1"#));
410        assert!(json.contains(r#""taint_critical":1"#));
411    }
412
413    #[test]
414    fn test_secure_finding_builder() {
415        let finding = SecureFinding::new("resource_leak", "high", "File not closed")
416            .with_location("src/db.py", 42);
417
418        assert_eq!(finding.category, "resource_leak");
419        assert_eq!(finding.severity, "high");
420        assert_eq!(finding.file, "src/db.py");
421        assert_eq!(finding.line, 42);
422    }
423}
424
425// =============================================================================
426// Explain Types
427// =============================================================================
428
429/// Parameter information.
430#[derive(Debug, Clone, Serialize, Deserialize)]
431pub struct ParamInfo {
432    /// Parameter name
433    pub name: String,
434    /// Type hint if present
435    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
436    pub type_hint: Option<String>,
437    /// Default value if present
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub default: Option<String>,
440}
441
442impl ParamInfo {
443    /// Create a new ParamInfo
444    pub fn new(name: impl Into<String>) -> Self {
445        Self {
446            name: name.into(),
447            type_hint: None,
448            default: None,
449        }
450    }
451
452    /// Set type hint
453    pub fn with_type(mut self, type_hint: impl Into<String>) -> Self {
454        self.type_hint = Some(type_hint.into());
455        self
456    }
457
458    /// Set default value
459    pub fn with_default(mut self, default: impl Into<String>) -> Self {
460        self.default = Some(default.into());
461        self
462    }
463}
464
465/// Function signature information.
466#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct SignatureInfo {
468    /// Parameters
469    pub params: Vec<ParamInfo>,
470    /// Return type if annotated
471    #[serde(skip_serializing_if = "Option::is_none")]
472    pub return_type: Option<String>,
473    /// Decorators
474    #[serde(default)]
475    pub decorators: Vec<String>,
476    /// Whether the function is async
477    #[serde(default)]
478    pub is_async: bool,
479    /// Docstring content
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub docstring: Option<String>,
482}
483
484impl SignatureInfo {
485    /// Create a new SignatureInfo
486    pub fn new() -> Self {
487        Self {
488            params: Vec::new(),
489            return_type: None,
490            decorators: Vec::new(),
491            is_async: false,
492            docstring: None,
493        }
494    }
495
496    /// Add a parameter
497    pub fn with_param(mut self, param: ParamInfo) -> Self {
498        self.params.push(param);
499        self
500    }
501
502    /// Set return type
503    pub fn with_return_type(mut self, return_type: impl Into<String>) -> Self {
504        self.return_type = Some(return_type.into());
505        self
506    }
507
508    /// Set docstring
509    pub fn with_docstring(mut self, docstring: impl Into<String>) -> Self {
510        self.docstring = Some(docstring.into());
511        self
512    }
513
514    /// Set async flag
515    pub fn set_async(mut self, is_async: bool) -> Self {
516        self.is_async = is_async;
517        self
518    }
519}
520
521impl Default for SignatureInfo {
522    fn default() -> Self {
523        Self::new()
524    }
525}
526
527/// Purity analysis result.
528#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct PurityInfo {
530    /// Classification: "pure", "impure", or "unknown"
531    pub classification: String,
532    /// List of detected effects (empty if pure)
533    #[serde(default)]
534    pub effects: Vec<String>,
535    /// Confidence: "high", "medium", or "low"
536    pub confidence: String,
537}
538
539impl PurityInfo {
540    /// Create a pure classification
541    pub fn pure() -> Self {
542        Self {
543            classification: "pure".to_string(),
544            effects: Vec::new(),
545            confidence: "high".to_string(),
546        }
547    }
548
549    /// Create an impure classification
550    pub fn impure(effects: Vec<String>) -> Self {
551        Self {
552            classification: "impure".to_string(),
553            effects,
554            confidence: "high".to_string(),
555        }
556    }
557
558    /// Create an unknown classification
559    pub fn unknown() -> Self {
560        Self {
561            classification: "unknown".to_string(),
562            effects: Vec::new(),
563            confidence: "low".to_string(),
564        }
565    }
566
567    /// Set confidence level
568    pub fn with_confidence(mut self, confidence: impl Into<String>) -> Self {
569        self.confidence = confidence.into();
570        self
571    }
572}
573
574impl Default for PurityInfo {
575    fn default() -> Self {
576        Self::unknown()
577    }
578}
579
580/// Complexity metrics.
581#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct ComplexityInfo {
583    /// Cyclomatic complexity
584    pub cyclomatic: u32,
585    /// Number of basic blocks
586    pub num_blocks: u32,
587    /// Number of edges in CFG
588    pub num_edges: u32,
589    /// Whether the function contains loops
590    pub has_loops: bool,
591}
592
593impl ComplexityInfo {
594    /// Create new complexity info
595    pub fn new(cyclomatic: u32, num_blocks: u32, num_edges: u32, has_loops: bool) -> Self {
596        Self {
597            cyclomatic,
598            num_blocks,
599            num_edges,
600            has_loops,
601        }
602    }
603}
604
605impl Default for ComplexityInfo {
606    fn default() -> Self {
607        Self {
608            cyclomatic: 1,
609            num_blocks: 1,
610            num_edges: 0,
611            has_loops: false,
612        }
613    }
614}
615
616/// Caller/callee information.
617#[derive(Debug, Clone, Serialize, Deserialize)]
618pub struct CallInfo {
619    /// Function name
620    pub name: String,
621    /// File path
622    pub file: String,
623    /// Line number
624    pub line: u32,
625}
626
627impl CallInfo {
628    /// Create new call info
629    pub fn new(name: impl Into<String>, file: impl Into<String>, line: u32) -> Self {
630        Self {
631            name: name.into(),
632            file: file.into(),
633            line,
634        }
635    }
636}
637
638/// Full explain report for a function.
639#[derive(Debug, Clone, Serialize, Deserialize)]
640pub struct ExplainReport {
641    /// Function name
642    pub function_name: String,
643    /// File path
644    pub file: String,
645    /// Start line of function
646    pub line_start: u32,
647    /// End line of function
648    pub line_end: u32,
649    /// Detected language
650    pub language: String,
651    /// Signature information
652    pub signature: SignatureInfo,
653    /// Purity analysis
654    pub purity: PurityInfo,
655    /// Complexity metrics (if available)
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub complexity: Option<ComplexityInfo>,
658    /// Functions that call this function
659    #[serde(default)]
660    pub callers: Vec<CallInfo>,
661    /// Functions called by this function
662    #[serde(default)]
663    pub callees: Vec<CallInfo>,
664}
665
666impl ExplainReport {
667    /// Create a new ExplainReport
668    pub fn new(
669        function_name: impl Into<String>,
670        file: impl Into<String>,
671        line_start: u32,
672        line_end: u32,
673        language: impl Into<String>,
674    ) -> Self {
675        Self {
676            function_name: function_name.into(),
677            file: file.into(),
678            line_start,
679            line_end,
680            language: language.into(),
681            signature: SignatureInfo::default(),
682            purity: PurityInfo::default(),
683            complexity: None,
684            callers: Vec::new(),
685            callees: Vec::new(),
686        }
687    }
688
689    /// Set signature
690    pub fn with_signature(mut self, signature: SignatureInfo) -> Self {
691        self.signature = signature;
692        self
693    }
694
695    /// Set purity
696    pub fn with_purity(mut self, purity: PurityInfo) -> Self {
697        self.purity = purity;
698        self
699    }
700
701    /// Set complexity
702    pub fn with_complexity(mut self, complexity: ComplexityInfo) -> Self {
703        self.complexity = Some(complexity);
704        self
705    }
706
707    /// Add a caller
708    pub fn add_caller(&mut self, caller: CallInfo) {
709        self.callers.push(caller);
710    }
711
712    /// Add a callee
713    pub fn add_callee(&mut self, callee: CallInfo) {
714        self.callees.push(callee);
715    }
716}
717
718// =============================================================================
719// Definition Types
720// =============================================================================
721
722/// Symbol kind.
723#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum, Default)]
724#[serde(rename_all = "snake_case")]
725pub enum SymbolKind {
726    Function,
727    Class,
728    Method,
729    Variable,
730    Parameter,
731    Constant,
732    Module,
733    Type,
734    Interface,
735    Property,
736    #[default]
737    Unknown,
738}
739
740/// Symbol information.
741#[derive(Debug, Clone, Serialize, Deserialize)]
742pub struct SymbolInfo {
743    /// Symbol name
744    pub name: String,
745    /// Symbol kind
746    pub kind: SymbolKind,
747    /// Location in source
748    #[serde(skip_serializing_if = "Option::is_none")]
749    pub location: Option<Location>,
750    /// Type annotation
751    #[serde(skip_serializing_if = "Option::is_none")]
752    pub type_annotation: Option<String>,
753    /// Docstring
754    #[serde(skip_serializing_if = "Option::is_none")]
755    pub docstring: Option<String>,
756    /// Whether this is a builtin
757    #[serde(default)]
758    pub is_builtin: bool,
759    /// Module path
760    #[serde(skip_serializing_if = "Option::is_none")]
761    pub module: Option<String>,
762}
763
764impl SymbolInfo {
765    /// Create a new SymbolInfo
766    pub fn new(name: impl Into<String>, kind: SymbolKind) -> Self {
767        Self {
768            name: name.into(),
769            kind,
770            location: None,
771            type_annotation: None,
772            docstring: None,
773            is_builtin: false,
774            module: None,
775        }
776    }
777}
778
779/// Definition lookup result.
780#[derive(Debug, Clone, Serialize, Deserialize)]
781pub struct DefinitionResult {
782    /// Symbol information
783    pub symbol: SymbolInfo,
784    /// Definition location
785    #[serde(skip_serializing_if = "Option::is_none")]
786    pub definition: Option<Location>,
787    /// Type definition location (for types)
788    #[serde(skip_serializing_if = "Option::is_none")]
789    pub type_definition: Option<Location>,
790}
791
792// =============================================================================
793// Diff Types
794// =============================================================================
795
796/// Type of AST change.
797#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
798#[serde(rename_all = "snake_case")]
799pub enum ChangeType {
800    Insert,
801    Delete,
802    Update,
803    Move,
804    Rename,
805    Extract,
806    Inline,
807    Format,
808}
809
810/// Diff granularity level.
811#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum, Serialize, Deserialize)]
812#[serde(rename_all = "snake_case")]
813pub enum DiffGranularity {
814    /// Token-level diff (L1)
815    Token,
816    /// Expression-level diff (L2)
817    Expression,
818    /// Statement-level diff (L3)
819    Statement,
820    /// Function-level diff (L4) - default
821    #[default]
822    Function,
823    /// Class-level diff (L5)
824    Class,
825    /// File-level diff (L6)
826    File,
827    /// Module-level diff (L7)
828    Module,
829    /// Architecture-level diff (L8)
830    Architecture,
831}
832
833/// Base class changes for class-level diff.
834#[derive(Debug, Clone, Serialize, Deserialize)]
835pub struct BaseChanges {
836    /// Base classes added
837    pub added: Vec<String>,
838    /// Base classes removed
839    pub removed: Vec<String>,
840}
841
842/// Kind of AST node.
843#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
844#[serde(rename_all = "snake_case")]
845pub enum NodeKind {
846    Function,
847    Class,
848    Method,
849    Field,
850    Statement,
851    Expression,
852    Block,
853}
854
855/// A single AST change.
856#[derive(Debug, Clone, Serialize, Deserialize)]
857pub struct ASTChange {
858    /// Type of change
859    pub change_type: ChangeType,
860    /// Kind of node changed
861    pub node_kind: NodeKind,
862    /// Name of the changed element
863    #[serde(skip_serializing_if = "Option::is_none")]
864    pub name: Option<String>,
865    /// Old location
866    #[serde(skip_serializing_if = "Option::is_none")]
867    pub old_location: Option<Location>,
868    /// New location
869    #[serde(skip_serializing_if = "Option::is_none")]
870    pub new_location: Option<Location>,
871    /// Old text (for updates)
872    #[serde(skip_serializing_if = "Option::is_none")]
873    pub old_text: Option<String>,
874    /// New text (for updates)
875    #[serde(skip_serializing_if = "Option::is_none")]
876    pub new_text: Option<String>,
877    /// Similarity score (for moves/renames)
878    #[serde(skip_serializing_if = "Option::is_none")]
879    pub similarity: Option<f64>,
880    /// Nested member changes (for class-level diff)
881    #[serde(skip_serializing_if = "Option::is_none")]
882    pub children: Option<Vec<ASTChange>>,
883    /// Base class changes (for class-level diff)
884    #[serde(skip_serializing_if = "Option::is_none")]
885    pub base_changes: Option<BaseChanges>,
886}
887
888/// Diff summary statistics.
889#[derive(Debug, Clone, Default, Serialize, Deserialize)]
890pub struct DiffSummary {
891    /// Total changes
892    pub total_changes: u32,
893    /// Semantic changes (excluding format)
894    pub semantic_changes: u32,
895    /// Number of inserts
896    pub inserts: u32,
897    /// Number of deletes
898    pub deletes: u32,
899    /// Number of updates
900    pub updates: u32,
901    /// Number of moves
902    pub moves: u32,
903    /// Number of renames
904    pub renames: u32,
905    /// Number of format-only changes
906    pub formats: u32,
907    /// Number of extracts
908    pub extracts: u32,
909}
910
911// =============================================================================
912// L6: File-Level Types
913// =============================================================================
914
915/// L6: File-level structural fingerprint change.
916///
917/// Represents a single file's structural change between two directory snapshots.
918/// The fingerprint is a hash of sorted function/class signatures, so two files
919/// with the same structure but different formatting will produce the same hash.
920#[derive(Debug, Clone, Serialize, Deserialize)]
921pub struct FileLevelChange {
922    /// Relative path of the file within the compared directory
923    pub relative_path: String,
924    /// Type of change (Insert=added, Delete=removed, Update=modified)
925    pub change_type: ChangeType,
926    /// Structural fingerprint of the file in dir_a (None if added)
927    #[serde(skip_serializing_if = "Option::is_none")]
928    pub old_fingerprint: Option<u64>,
929    /// Structural fingerprint of the file in dir_b (None if removed)
930    #[serde(skip_serializing_if = "Option::is_none")]
931    pub new_fingerprint: Option<u64>,
932    /// Which signatures changed (only for Update)
933    #[serde(skip_serializing_if = "Option::is_none")]
934    pub signature_changes: Option<Vec<String>>,
935}
936
937// =============================================================================
938// L7: Module-Level Types
939// =============================================================================
940
941/// L7: An import edge in the module dependency graph.
942///
943/// Represents a single `from X import Y` or `import X` statement,
944/// capturing the source file, target module, and imported names.
945#[derive(Debug, Clone, Serialize, Deserialize)]
946pub struct ImportEdge {
947    /// Source file that contains the import statement
948    pub source_file: String,
949    /// Target module being imported from
950    pub target_module: String,
951    /// Specific names imported (empty for `import X`)
952    pub imported_names: Vec<String>,
953}
954
955/// L7: Module-level change combining import graph and structural diffs.
956#[derive(Debug, Clone, Serialize, Deserialize)]
957pub struct ModuleLevelChange {
958    /// Module path (relative file path)
959    pub module_path: String,
960    /// Type of change at the module level
961    pub change_type: ChangeType,
962    /// Import edges added in dir_b
963    pub imports_added: Vec<ImportEdge>,
964    /// Import edges removed from dir_a
965    pub imports_removed: Vec<ImportEdge>,
966    /// L6 file-level change data (if structure also changed)
967    #[serde(skip_serializing_if = "Option::is_none")]
968    pub file_change: Option<FileLevelChange>,
969}
970
971/// L7: Summary of import graph differences between two directories.
972#[derive(Debug, Clone, Serialize, Deserialize)]
973pub struct ImportGraphSummary {
974    /// Total import edges in dir_a
975    pub total_edges_a: usize,
976    /// Total import edges in dir_b
977    pub total_edges_b: usize,
978    /// Number of edges added (present in B but not A)
979    pub edges_added: usize,
980    /// Number of edges removed (present in A but not B)
981    pub edges_removed: usize,
982    /// Number of modules whose import set changed
983    pub modules_with_import_changes: usize,
984}
985
986// =============================================================================
987// L8: Architecture-Level Types
988// =============================================================================
989
990/// L8: Type of architectural change detected.
991#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
992pub enum ArchChangeType {
993    /// A directory migrated from one architectural layer to another
994    LayerMigration,
995    /// A new directory/layer was added
996    Added,
997    /// A directory/layer was removed
998    Removed,
999    /// The composition of a directory changed significantly
1000    CompositionChanged,
1001    /// A new dependency cycle was introduced
1002    CycleIntroduced,
1003    /// An existing dependency cycle was resolved
1004    CycleResolved,
1005}
1006
1007/// L8: Architecture-level change for a single directory.
1008#[derive(Debug, Clone, Serialize, Deserialize)]
1009pub struct ArchLevelChange {
1010    /// Directory path (relative)
1011    pub directory: String,
1012    /// Type of architectural change
1013    pub change_type: ArchChangeType,
1014    /// Previous layer classification (if applicable)
1015    #[serde(skip_serializing_if = "Option::is_none")]
1016    pub old_layer: Option<String>,
1017    /// New layer classification (if applicable)
1018    #[serde(skip_serializing_if = "Option::is_none")]
1019    pub new_layer: Option<String>,
1020    /// Functions that migrated between layers
1021    #[serde(default)]
1022    pub migrated_functions: Vec<String>,
1023}
1024
1025/// L8: Summary of architecture-level differences.
1026#[derive(Debug, Clone, Serialize, Deserialize)]
1027pub struct ArchDiffSummary {
1028    /// Number of directories that migrated between layers
1029    pub layer_migrations: usize,
1030    /// Number of new directories added
1031    pub directories_added: usize,
1032    /// Number of directories removed
1033    pub directories_removed: usize,
1034    /// Number of new dependency cycles introduced
1035    pub cycles_introduced: usize,
1036    /// Number of dependency cycles resolved
1037    pub cycles_resolved: usize,
1038    /// Overall stability score (1.0 = no changes, 0.0 = everything changed)
1039    pub stability_score: f64,
1040}
1041
1042// =============================================================================
1043// Diff Report
1044// =============================================================================
1045
1046/// Diff report.
1047#[derive(Debug, Clone, Serialize, Deserialize)]
1048pub struct DiffReport {
1049    /// First file
1050    pub file_a: String,
1051    /// Second file
1052    pub file_b: String,
1053    /// Whether files are identical
1054    pub identical: bool,
1055    /// List of changes
1056    pub changes: Vec<ASTChange>,
1057    /// Summary statistics
1058    #[serde(skip_serializing_if = "Option::is_none")]
1059    pub summary: Option<DiffSummary>,
1060    /// Granularity level of this diff
1061    #[serde(default)]
1062    pub granularity: DiffGranularity,
1063    /// L6: File-level structural changes (directory diff)
1064    #[serde(skip_serializing_if = "Option::is_none")]
1065    pub file_changes: Option<Vec<FileLevelChange>>,
1066    /// L7: Module-level changes with import graph diff
1067    #[serde(skip_serializing_if = "Option::is_none")]
1068    pub module_changes: Option<Vec<ModuleLevelChange>>,
1069    /// L7: Import graph summary
1070    #[serde(skip_serializing_if = "Option::is_none")]
1071    pub import_graph_summary: Option<ImportGraphSummary>,
1072    /// L8: Architecture-level changes
1073    #[serde(skip_serializing_if = "Option::is_none")]
1074    pub arch_changes: Option<Vec<ArchLevelChange>>,
1075    /// L8: Architecture diff summary
1076    #[serde(skip_serializing_if = "Option::is_none")]
1077    pub arch_summary: Option<ArchDiffSummary>,
1078}
1079
1080impl DiffReport {
1081    /// Create a new DiffReport
1082    pub fn new(file_a: impl Into<String>, file_b: impl Into<String>) -> Self {
1083        Self {
1084            file_a: file_a.into(),
1085            file_b: file_b.into(),
1086            identical: true,
1087            changes: Vec::new(),
1088            summary: Some(DiffSummary::default()),
1089            granularity: DiffGranularity::Function,
1090            file_changes: None,
1091            module_changes: None,
1092            import_graph_summary: None,
1093            arch_changes: None,
1094            arch_summary: None,
1095        }
1096    }
1097}
1098
1099// =============================================================================
1100// Diff Impact Types
1101// =============================================================================
1102
1103/// A function affected by changes.
1104#[derive(Debug, Clone, Serialize, Deserialize)]
1105pub struct ChangedFunction {
1106    /// Function name
1107    pub name: String,
1108    /// File path
1109    pub file: String,
1110    /// Line number
1111    pub line: u32,
1112    /// Functions that call this function
1113    #[serde(default)]
1114    pub callers: Vec<CallInfo>,
1115}
1116
1117/// Diff impact summary.
1118#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1119pub struct DiffImpactSummary {
1120    /// Number of files changed
1121    pub files_changed: u32,
1122    /// Number of functions changed
1123    pub functions_changed: u32,
1124    /// Number of tests to run
1125    pub tests_to_run: u32,
1126}
1127
1128/// Diff impact report.
1129#[derive(Debug, Clone, Serialize, Deserialize)]
1130pub struct DiffImpactReport {
1131    /// Changed functions
1132    pub changed_functions: Vec<ChangedFunction>,
1133    /// Suggested tests to run
1134    pub suggested_tests: Vec<String>,
1135    /// Summary
1136    pub summary: DiffImpactSummary,
1137}
1138
1139impl DiffImpactReport {
1140    /// Create a new DiffImpactReport
1141    pub fn new() -> Self {
1142        Self {
1143            changed_functions: Vec::new(),
1144            suggested_tests: Vec::new(),
1145            summary: DiffImpactSummary::default(),
1146        }
1147    }
1148}
1149
1150impl Default for DiffImpactReport {
1151    fn default() -> Self {
1152        Self::new()
1153    }
1154}
1155
1156// =============================================================================
1157// API Check Types
1158// =============================================================================
1159
1160/// Categories of API misuse.
1161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
1162#[serde(rename_all = "snake_case")]
1163pub enum MisuseCategory {
1164    CallOrder,
1165    ErrorHandling,
1166    Parameters,
1167    Resources,
1168    Crypto,
1169    Concurrency,
1170    Security,
1171}
1172
1173/// Severity of API misuse.
1174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
1175#[serde(rename_all = "snake_case")]
1176pub enum MisuseSeverity {
1177    Info,
1178    Low,
1179    Medium,
1180    High,
1181}
1182
1183/// An API rule definition.
1184#[derive(Debug, Clone, Serialize, Deserialize)]
1185pub struct APIRule {
1186    /// Rule identifier
1187    pub id: String,
1188    /// Rule name
1189    pub name: String,
1190    /// Category
1191    pub category: MisuseCategory,
1192    /// Severity
1193    pub severity: MisuseSeverity,
1194    /// Description
1195    pub description: String,
1196    /// Example of correct usage
1197    pub correct_usage: String,
1198}
1199
1200/// A detected API misuse.
1201#[derive(Debug, Clone, Serialize, Deserialize)]
1202pub struct MisuseFinding {
1203    /// File path
1204    pub file: String,
1205    /// Line number
1206    pub line: u32,
1207    /// Column number
1208    pub column: u32,
1209    /// Rule that was violated
1210    pub rule: APIRule,
1211    /// The API call that violated the rule
1212    pub api_call: String,
1213    /// Human-readable message
1214    pub message: String,
1215    /// Suggested fix
1216    pub fix_suggestion: String,
1217    /// Code context
1218    #[serde(default)]
1219    pub code_context: String,
1220}
1221
1222/// API check summary.
1223#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1224pub struct APICheckSummary {
1225    /// Total findings
1226    pub total_findings: u32,
1227    /// Findings by category
1228    #[serde(default)]
1229    pub by_category: HashMap<String, u32>,
1230    /// Findings by severity
1231    #[serde(default)]
1232    pub by_severity: HashMap<String, u32>,
1233    /// APIs that were checked
1234    #[serde(default)]
1235    pub apis_checked: Vec<String>,
1236    /// Number of files scanned
1237    pub files_scanned: u32,
1238}
1239
1240/// API check report.
1241#[derive(Debug, Clone, Serialize, Deserialize)]
1242pub struct APICheckReport {
1243    /// Findings
1244    pub findings: Vec<MisuseFinding>,
1245    /// Summary
1246    pub summary: APICheckSummary,
1247    /// Number of rules applied
1248    pub rules_applied: u32,
1249}
1250
1251impl APICheckReport {
1252    /// Create a new APICheckReport
1253    pub fn new() -> Self {
1254        Self {
1255            findings: Vec::new(),
1256            summary: APICheckSummary::default(),
1257            rules_applied: 0,
1258        }
1259    }
1260}
1261
1262impl Default for APICheckReport {
1263    fn default() -> Self {
1264        Self::new()
1265    }
1266}
1267
1268// =============================================================================
1269// Equivalence (GVN) Types
1270// =============================================================================
1271
1272/// Reference to an expression in source code.
1273#[derive(Debug, Clone, Serialize, Deserialize)]
1274pub struct ExpressionRef {
1275    /// Expression text
1276    pub text: String,
1277    /// Line number
1278    pub line: u32,
1279    /// Value number (GVN)
1280    pub value_number: u32,
1281}
1282
1283/// A group of expressions sharing the same value number.
1284#[derive(Debug, Clone, Serialize, Deserialize)]
1285pub struct GVNEquivalence {
1286    /// Value number for this group
1287    pub value_number: u32,
1288    /// Expressions in this equivalence class
1289    pub expressions: Vec<ExpressionRef>,
1290    /// Reason for equivalence
1291    #[serde(default)]
1292    pub reason: String,
1293}
1294
1295/// A redundant expression pair.
1296#[derive(Debug, Clone, Serialize, Deserialize)]
1297pub struct Redundancy {
1298    /// Original expression
1299    pub original: ExpressionRef,
1300    /// Redundant expression
1301    pub redundant: ExpressionRef,
1302    /// Reason why it's redundant
1303    #[serde(default)]
1304    pub reason: String,
1305}
1306
1307/// GVN summary statistics.
1308#[derive(Debug, Clone, Serialize, Deserialize)]
1309pub struct GVNSummary {
1310    /// Total expressions analyzed
1311    pub total_expressions: u32,
1312    /// Unique values (value numbers)
1313    pub unique_values: u32,
1314    /// Compression ratio (unique/total)
1315    pub compression_ratio: f64,
1316}
1317
1318impl Default for GVNSummary {
1319    fn default() -> Self {
1320        Self {
1321            total_expressions: 0,
1322            unique_values: 0,
1323            compression_ratio: 1.0,
1324        }
1325    }
1326}
1327
1328/// GVN report for a function.
1329#[derive(Debug, Clone, Serialize, Deserialize)]
1330pub struct GVNReport {
1331    /// Function name
1332    pub function: String,
1333    /// Equivalence classes
1334    #[serde(default)]
1335    pub equivalences: Vec<GVNEquivalence>,
1336    /// Redundant expressions
1337    #[serde(default)]
1338    pub redundancies: Vec<Redundancy>,
1339    /// Summary statistics
1340    pub summary: GVNSummary,
1341}
1342
1343impl GVNReport {
1344    /// Create a new GVNReport
1345    pub fn new(function: impl Into<String>) -> Self {
1346        Self {
1347            function: function.into(),
1348            equivalences: Vec::new(),
1349            redundancies: Vec::new(),
1350            summary: GVNSummary::default(),
1351        }
1352    }
1353}
1354
1355// =============================================================================
1356// Vuln Types
1357// =============================================================================
1358
1359/// Types of vulnerabilities detected.
1360#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ValueEnum)]
1361#[serde(rename_all = "snake_case")]
1362#[value(rename_all = "snake_case")]
1363pub enum VulnType {
1364    SqlInjection,
1365    Xss,
1366    CommandInjection,
1367    Ssrf,
1368    PathTraversal,
1369    Deserialization,
1370    UnsafeCode,
1371    MemorySafety,
1372    Panic,
1373    Xxe,
1374    OpenRedirect,
1375    LdapInjection,
1376    XpathInjection,
1377}
1378
1379impl VulnType {
1380    /// Get the CWE identifier for this vulnerability type.
1381    pub fn cwe_id(&self) -> &'static str {
1382        match self {
1383            Self::SqlInjection => "CWE-89",
1384            Self::Xss => "CWE-79",
1385            Self::CommandInjection => "CWE-78",
1386            Self::Ssrf => "CWE-918",
1387            Self::PathTraversal => "CWE-22",
1388            Self::Deserialization => "CWE-502",
1389            Self::UnsafeCode => "CWE-242",
1390            Self::MemorySafety => "CWE-119",
1391            Self::Panic => "CWE-703",
1392            Self::Xxe => "CWE-611",
1393            Self::OpenRedirect => "CWE-601",
1394            Self::LdapInjection => "CWE-90",
1395            Self::XpathInjection => "CWE-643",
1396        }
1397    }
1398
1399    /// Get the default severity for this vulnerability type.
1400    pub fn default_severity(&self) -> Severity {
1401        match self {
1402            Self::SqlInjection
1403            | Self::CommandInjection
1404            | Self::Deserialization
1405            | Self::MemorySafety => Severity::Critical,
1406            Self::Xxe
1407            | Self::Xss
1408            | Self::Ssrf
1409            | Self::PathTraversal
1410            | Self::LdapInjection
1411            | Self::XpathInjection
1412            | Self::UnsafeCode => Severity::High,
1413            Self::OpenRedirect | Self::Panic => Severity::Medium,
1414        }
1415    }
1416}
1417
1418/// A step in the taint flow.
1419#[derive(Debug, Clone, Serialize, Deserialize)]
1420pub struct TaintFlow {
1421    /// File path
1422    pub file: String,
1423    /// Line number
1424    pub line: u32,
1425    /// Column number
1426    pub column: u32,
1427    /// Code snippet
1428    pub code_snippet: String,
1429    /// Description of this step
1430    pub description: String,
1431}
1432
1433/// A vulnerability finding.
1434#[derive(Debug, Clone, Serialize, Deserialize)]
1435pub struct VulnFinding {
1436    /// Vulnerability type
1437    pub vuln_type: VulnType,
1438    /// Severity
1439    pub severity: Severity,
1440    /// CWE identifier
1441    pub cwe_id: String,
1442    /// Title
1443    pub title: String,
1444    /// Description
1445    pub description: String,
1446    /// File path
1447    pub file: String,
1448    /// Line number
1449    pub line: u32,
1450    /// Column number
1451    pub column: u32,
1452    /// Taint flow from source to sink
1453    pub taint_flow: Vec<TaintFlow>,
1454    /// Remediation advice
1455    pub remediation: String,
1456    /// Confidence score (0.0-1.0)
1457    pub confidence: f64,
1458}
1459
1460/// Vulnerability summary statistics.
1461#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1462pub struct VulnSummary {
1463    /// Total findings
1464    pub total_findings: u32,
1465    /// Findings by severity
1466    #[serde(default)]
1467    pub by_severity: HashMap<String, u32>,
1468    /// Findings by type
1469    #[serde(default)]
1470    pub by_type: HashMap<String, u32>,
1471    /// Files with vulnerabilities
1472    pub files_with_vulns: u32,
1473}
1474
1475/// Vulnerability report.
1476#[derive(Debug, Clone, Serialize, Deserialize)]
1477pub struct VulnReport {
1478    /// Findings
1479    pub findings: Vec<VulnFinding>,
1480    /// Summary (optional for incremental results)
1481    #[serde(skip_serializing_if = "Option::is_none")]
1482    pub summary: Option<VulnSummary>,
1483    /// Scan duration in milliseconds
1484    pub scan_duration_ms: u64,
1485    /// Number of files scanned
1486    pub files_scanned: u32,
1487}
1488
1489impl VulnReport {
1490    /// Create a new VulnReport
1491    pub fn new() -> Self {
1492        Self {
1493            findings: Vec::new(),
1494            summary: None,
1495            scan_duration_ms: 0,
1496            files_scanned: 0,
1497        }
1498    }
1499}
1500
1501impl Default for VulnReport {
1502    fn default() -> Self {
1503        Self::new()
1504    }
1505}
1506
1507// =============================================================================
1508// Unit Tests for Types
1509// =============================================================================
1510
1511#[cfg(test)]
1512mod unit_types_tests {
1513    use super::*;
1514
1515    // =========================================================================
1516    // Output Format Tests
1517    // =========================================================================
1518
1519    #[test]
1520    fn test_output_format_serialization() {
1521        let json = serde_json::to_string(&OutputFormat::Json).unwrap();
1522        assert_eq!(json, r#""json""#);
1523
1524        let text = serde_json::to_string(&OutputFormat::Text).unwrap();
1525        assert_eq!(text, r#""text""#);
1526
1527        let sarif = serde_json::to_string(&OutputFormat::Sarif).unwrap();
1528        assert_eq!(sarif, r#""sarif""#);
1529    }
1530
1531    #[test]
1532    fn test_output_format_deserialization() {
1533        let json: OutputFormat = serde_json::from_str(r#""json""#).unwrap();
1534        assert_eq!(json, OutputFormat::Json);
1535
1536        let text: OutputFormat = serde_json::from_str(r#""text""#).unwrap();
1537        assert_eq!(text, OutputFormat::Text);
1538    }
1539
1540    // =========================================================================
1541    // Severity Tests
1542    // =========================================================================
1543
1544    #[test]
1545    fn test_severity_ordering() {
1546        assert!(Severity::Critical.order() < Severity::High.order());
1547        assert!(Severity::High.order() < Severity::Medium.order());
1548        assert!(Severity::Medium.order() < Severity::Low.order());
1549        assert!(Severity::Low.order() < Severity::Info.order());
1550    }
1551
1552    #[test]
1553    fn test_severity_serialization() {
1554        let critical = serde_json::to_string(&Severity::Critical).unwrap();
1555        assert_eq!(critical, r#""critical""#);
1556
1557        let info = serde_json::to_string(&Severity::Info).unwrap();
1558        assert_eq!(info, r#""info""#);
1559    }
1560
1561    // =========================================================================
1562    // Location Tests
1563    // =========================================================================
1564
1565    #[test]
1566    fn test_location_serialization() {
1567        let loc = Location::new("test.py", 42);
1568        let json = serde_json::to_string(&loc).unwrap();
1569        assert!(json.contains(r#""file":"test.py""#));
1570        assert!(json.contains(r#""line":42"#));
1571    }
1572
1573    #[test]
1574    fn test_location_with_column() {
1575        let loc = Location::with_column("test.py", 42, 10);
1576        assert_eq!(loc.column, 10);
1577    }
1578
1579    // =========================================================================
1580    // Todo Types Tests
1581    // =========================================================================
1582
1583    #[test]
1584    fn test_todo_report_serialization() {
1585        let mut report = TodoReport::new("/path/to/file.py");
1586        report
1587            .items
1588            .push(TodoItem::new("dead_code", 1, "Unused function"));
1589        report.summary.dead_count = 1;
1590
1591        let json = serde_json::to_string(&report).unwrap();
1592        assert!(json.contains(r#""wrapper":"todo""#));
1593        assert!(json.contains(r#""dead_count":1"#));
1594    }
1595
1596    #[test]
1597    fn test_todo_item_builder() {
1598        let item = TodoItem::new("complexity", 2, "High cyclomatic complexity")
1599            .with_location("src/main.py", 100)
1600            .with_severity("high")
1601            .with_score(0.85);
1602
1603        assert_eq!(item.category, "complexity");
1604        assert_eq!(item.file, "src/main.py");
1605        assert_eq!(item.line, 100);
1606        assert_eq!(item.severity, "high");
1607        assert!((item.score - 0.85).abs() < 0.001);
1608    }
1609
1610    // =========================================================================
1611    // Explain Types Tests
1612    // =========================================================================
1613
1614    #[test]
1615    fn test_explain_report_serialization() {
1616        let mut report = ExplainReport::new("calculate_total", "/path/file.py", 10, 20, "python");
1617        report.purity = PurityInfo::pure();
1618
1619        let json = serde_json::to_string(&report).unwrap();
1620        assert!(json.contains(r#""function_name":"calculate_total""#));
1621        assert!(json.contains(r#""classification":"pure""#));
1622    }
1623
1624    #[test]
1625    fn test_signature_info_builder() {
1626        let sig = SignatureInfo::new()
1627            .with_param(ParamInfo::new("x").with_type("int"))
1628            .with_return_type("int")
1629            .with_docstring("Doubles the input");
1630
1631        assert_eq!(sig.params.len(), 1);
1632        assert_eq!(sig.params[0].name, "x");
1633        assert_eq!(sig.return_type.unwrap(), "int");
1634    }
1635
1636    // =========================================================================
1637    // Secure Types Tests
1638    // =========================================================================
1639
1640    #[test]
1641    fn test_secure_report_serialization() {
1642        let mut report = SecureReport::new("/path/to/file.py");
1643        report
1644            .findings
1645            .push(SecureFinding::new("taint", "critical", "SQL injection"));
1646
1647        let json = serde_json::to_string(&report).unwrap();
1648        assert!(json.contains(r#""wrapper":"secure""#));
1649    }
1650
1651    // =========================================================================
1652    // Definition Types Tests
1653    // =========================================================================
1654
1655    #[test]
1656    fn test_definition_result_serialization() {
1657        let result = DefinitionResult {
1658            symbol: SymbolInfo::new("my_func", SymbolKind::Function),
1659            definition: Some(Location::new("file.py", 10)),
1660            type_definition: None,
1661        };
1662
1663        let json = serde_json::to_string(&result).unwrap();
1664        assert!(json.contains(r#""name":"my_func""#));
1665        assert!(json.contains(r#""kind":"function""#));
1666    }
1667
1668    #[test]
1669    fn test_symbol_kind_serialization() {
1670        let kind = SymbolKind::Function;
1671        let json = serde_json::to_string(&kind).unwrap();
1672        assert_eq!(json, r#""function""#);
1673    }
1674
1675    // =========================================================================
1676    // Diff Types Tests
1677    // =========================================================================
1678
1679    #[test]
1680    fn test_diff_report_serialization() {
1681        let mut report = DiffReport::new("a.py", "b.py");
1682        report.identical = false;
1683        if let Some(ref mut summary) = report.summary {
1684            summary.inserts = 1;
1685        }
1686
1687        let json = serde_json::to_string(&report).unwrap();
1688        assert!(json.contains(r#""file_a":"a.py""#));
1689        assert!(json.contains(r#""identical":false"#));
1690    }
1691
1692    #[test]
1693    fn test_change_type_serialization() {
1694        let insert = serde_json::to_string(&ChangeType::Insert).unwrap();
1695        assert_eq!(insert, r#""insert""#);
1696
1697        let rename = serde_json::to_string(&ChangeType::Rename).unwrap();
1698        assert_eq!(rename, r#""rename""#);
1699    }
1700
1701    // =========================================================================
1702    // API Check Types Tests
1703    // =========================================================================
1704
1705    #[test]
1706    fn test_api_check_report_serialization() {
1707        let mut report = APICheckReport::new();
1708        report.rules_applied = 5;
1709        report.summary.total_findings = 2;
1710
1711        let json = serde_json::to_string(&report).unwrap();
1712        assert!(json.contains(r#""rules_applied":5"#));
1713        assert!(json.contains(r#""total_findings":2"#));
1714    }
1715
1716    // =========================================================================
1717    // GVN Types Tests
1718    // =========================================================================
1719
1720    #[test]
1721    fn test_gvn_report_serialization() {
1722        let mut report = GVNReport::new("test_func");
1723        report.summary.total_expressions = 10;
1724        report.summary.unique_values = 7;
1725        report.summary.compression_ratio = 0.7;
1726
1727        let json = serde_json::to_string(&report).unwrap();
1728        assert!(json.contains(r#""function":"test_func""#));
1729        assert!(json.contains(r#""compression_ratio":0.7"#));
1730    }
1731
1732    // =========================================================================
1733    // Vuln Types Tests
1734    // =========================================================================
1735
1736    #[test]
1737    fn test_vuln_report_serialization() {
1738        let mut report = VulnReport::new();
1739        report.files_scanned = 5;
1740        report.scan_duration_ms = 100;
1741
1742        let json = serde_json::to_string(&report).unwrap();
1743        assert!(json.contains(r#""files_scanned":5"#));
1744        assert!(json.contains(r#""scan_duration_ms":100"#));
1745    }
1746
1747    #[test]
1748    fn test_vuln_type_cwe_mapping() {
1749        assert_eq!(VulnType::SqlInjection.cwe_id(), "CWE-89");
1750        assert_eq!(VulnType::Xss.cwe_id(), "CWE-79");
1751        assert_eq!(VulnType::CommandInjection.cwe_id(), "CWE-78");
1752        assert_eq!(VulnType::Ssrf.cwe_id(), "CWE-918");
1753        assert_eq!(VulnType::PathTraversal.cwe_id(), "CWE-22");
1754        assert_eq!(VulnType::Deserialization.cwe_id(), "CWE-502");
1755        assert_eq!(VulnType::UnsafeCode.cwe_id(), "CWE-242");
1756        assert_eq!(VulnType::MemorySafety.cwe_id(), "CWE-119");
1757        assert_eq!(VulnType::Panic.cwe_id(), "CWE-703");
1758        assert_eq!(VulnType::Xxe.cwe_id(), "CWE-611");
1759        assert_eq!(VulnType::OpenRedirect.cwe_id(), "CWE-601");
1760        assert_eq!(VulnType::LdapInjection.cwe_id(), "CWE-90");
1761        assert_eq!(VulnType::XpathInjection.cwe_id(), "CWE-643");
1762    }
1763
1764    #[test]
1765    fn test_vuln_type_default_severity() {
1766        assert_eq!(
1767            VulnType::SqlInjection.default_severity(),
1768            Severity::Critical
1769        );
1770        assert_eq!(
1771            VulnType::CommandInjection.default_severity(),
1772            Severity::Critical
1773        );
1774        assert_eq!(
1775            VulnType::MemorySafety.default_severity(),
1776            Severity::Critical
1777        );
1778        assert_eq!(VulnType::Xss.default_severity(), Severity::High);
1779        assert_eq!(VulnType::UnsafeCode.default_severity(), Severity::High);
1780        assert_eq!(VulnType::OpenRedirect.default_severity(), Severity::Medium);
1781        assert_eq!(VulnType::Panic.default_severity(), Severity::Medium);
1782    }
1783
1784    #[test]
1785    fn test_vuln_type_serialization() {
1786        let sql_inj = serde_json::to_string(&VulnType::SqlInjection).unwrap();
1787        assert_eq!(sql_inj, r#""sql_injection""#);
1788
1789        let xss = serde_json::to_string(&VulnType::Xss).unwrap();
1790        assert_eq!(xss, r#""xss""#);
1791    }
1792}