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