1use std::collections::HashMap;
7
8use clap::ValueEnum;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum OutputFormat {
20 #[default]
22 Json,
23 Text,
25 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#[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 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#[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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct TodoItem {
128 pub category: String,
130 pub priority: u32,
132 pub description: String,
134 #[serde(default)]
136 pub file: String,
137 #[serde(default)]
139 pub line: u32,
140 #[serde(default)]
142 pub severity: String,
143 #[serde(default)]
145 pub score: f64,
146}
147
148impl TodoItem {
149 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 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 pub fn with_severity(mut self, severity: impl Into<String>) -> Self {
171 self.severity = severity.into();
172 self
173 }
174
175 pub fn with_score(mut self, score: f64) -> Self {
177 self.score = score;
178 self
179 }
180}
181
182#[derive(Debug, Clone, Default, Serialize, Deserialize)]
184pub struct TodoSummary {
185 pub dead_count: u32,
187 pub similar_pairs: u32,
189 pub low_cohesion_count: u32,
191 pub hotspot_count: u32,
193 pub equivalence_groups: u32,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct TodoReport {
200 pub wrapper: String,
202 pub path: String,
204 pub items: Vec<TodoItem>,
206 pub summary: TodoSummary,
208 #[serde(default)]
210 pub sub_results: HashMap<String, Value>,
211 pub total_elapsed_ms: f64,
213}
214
215impl TodoReport {
216 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#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct SecureFinding {
236 pub category: String,
238 pub severity: String,
240 pub description: String,
242 #[serde(default)]
244 pub file: String,
245 #[serde(default)]
247 pub line: u32,
248}
249
250impl SecureFinding {
251 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 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
276pub struct SecureSummary {
277 pub taint_count: u32,
279 pub taint_critical: u32,
281 pub leak_count: u32,
283 pub bounds_warnings: u32,
285 pub missing_contracts: u32,
287 pub mutable_params: u32,
289 #[serde(default)]
291 pub unsafe_blocks: u32,
292 #[serde(default)]
294 pub raw_pointer_ops: u32,
295 #[serde(default)]
297 pub unwrap_calls: u32,
298 #[serde(default)]
300 pub todo_markers: u32,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct SecureReport {
306 pub wrapper: String,
308 pub path: String,
310 pub findings: Vec<SecureFinding>,
312 pub summary: SecureSummary,
314 #[serde(default)]
316 pub sub_results: HashMap<String, Value>,
317 pub total_elapsed_ms: f64,
319}
320
321impl SecureReport {
322 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
431pub struct ParamInfo {
432 pub name: String,
434 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
436 pub type_hint: Option<String>,
437 #[serde(skip_serializing_if = "Option::is_none")]
439 pub default: Option<String>,
440}
441
442impl ParamInfo {
443 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 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 pub fn with_default(mut self, default: impl Into<String>) -> Self {
460 self.default = Some(default.into());
461 self
462 }
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct SignatureInfo {
468 pub params: Vec<ParamInfo>,
470 #[serde(skip_serializing_if = "Option::is_none")]
472 pub return_type: Option<String>,
473 #[serde(default)]
475 pub decorators: Vec<String>,
476 #[serde(default)]
478 pub is_async: bool,
479 #[serde(skip_serializing_if = "Option::is_none")]
481 pub docstring: Option<String>,
482}
483
484impl SignatureInfo {
485 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 pub fn with_param(mut self, param: ParamInfo) -> Self {
498 self.params.push(param);
499 self
500 }
501
502 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 pub fn with_docstring(mut self, docstring: impl Into<String>) -> Self {
510 self.docstring = Some(docstring.into());
511 self
512 }
513
514 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#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct PurityInfo {
530 pub classification: String,
532 #[serde(default)]
534 pub effects: Vec<String>,
535 pub confidence: String,
537}
538
539impl PurityInfo {
540 pub fn pure() -> Self {
542 Self {
543 classification: "pure".to_string(),
544 effects: Vec::new(),
545 confidence: "high".to_string(),
546 }
547 }
548
549 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 pub fn unknown() -> Self {
560 Self {
561 classification: "unknown".to_string(),
562 effects: Vec::new(),
563 confidence: "low".to_string(),
564 }
565 }
566
567 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#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct ComplexityInfo {
583 pub cyclomatic: u32,
585 pub num_blocks: u32,
587 pub num_edges: u32,
589 pub has_loops: bool,
591}
592
593impl ComplexityInfo {
594 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#[derive(Debug, Clone, Serialize, Deserialize)]
618pub struct CallInfo {
619 pub name: String,
621 pub file: String,
623 pub line: u32,
625}
626
627impl CallInfo {
628 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#[derive(Debug, Clone, Serialize, Deserialize)]
640pub struct ExplainReport {
641 pub function_name: String,
643 pub file: String,
645 pub line_start: u32,
647 pub line_end: u32,
649 pub language: String,
651 pub signature: SignatureInfo,
653 pub purity: PurityInfo,
655 #[serde(skip_serializing_if = "Option::is_none")]
657 pub complexity: Option<ComplexityInfo>,
658 #[serde(default)]
660 pub callers: Vec<CallInfo>,
661 #[serde(default)]
663 pub callees: Vec<CallInfo>,
664}
665
666impl ExplainReport {
667 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 pub fn with_signature(mut self, signature: SignatureInfo) -> Self {
691 self.signature = signature;
692 self
693 }
694
695 pub fn with_purity(mut self, purity: PurityInfo) -> Self {
697 self.purity = purity;
698 self
699 }
700
701 pub fn with_complexity(mut self, complexity: ComplexityInfo) -> Self {
703 self.complexity = Some(complexity);
704 self
705 }
706
707 pub fn add_caller(&mut self, caller: CallInfo) {
709 self.callers.push(caller);
710 }
711
712 pub fn add_callee(&mut self, callee: CallInfo) {
714 self.callees.push(callee);
715 }
716}
717
718#[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#[derive(Debug, Clone, Serialize, Deserialize)]
742pub struct SymbolInfo {
743 pub name: String,
745 pub kind: SymbolKind,
747 #[serde(skip_serializing_if = "Option::is_none")]
749 pub location: Option<Location>,
750 #[serde(skip_serializing_if = "Option::is_none")]
752 pub type_annotation: Option<String>,
753 #[serde(skip_serializing_if = "Option::is_none")]
755 pub docstring: Option<String>,
756 #[serde(default)]
758 pub is_builtin: bool,
759 #[serde(skip_serializing_if = "Option::is_none")]
761 pub module: Option<String>,
762}
763
764impl SymbolInfo {
765 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#[derive(Debug, Clone, Serialize, Deserialize)]
781pub struct DefinitionResult {
782 pub symbol: SymbolInfo,
784 #[serde(skip_serializing_if = "Option::is_none")]
786 pub definition: Option<Location>,
787 #[serde(skip_serializing_if = "Option::is_none")]
789 pub type_definition: Option<Location>,
790}
791
792#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum, Serialize, Deserialize)]
812#[serde(rename_all = "snake_case")]
813pub enum DiffGranularity {
814 Token,
816 Expression,
818 Statement,
820 #[default]
822 Function,
823 Class,
825 File,
827 Module,
829 Architecture,
831}
832
833#[derive(Debug, Clone, Serialize, Deserialize)]
835pub struct BaseChanges {
836 pub added: Vec<String>,
838 pub removed: Vec<String>,
840}
841
842#[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#[derive(Debug, Clone, Serialize, Deserialize)]
857pub struct ASTChange {
858 pub change_type: ChangeType,
860 pub node_kind: NodeKind,
862 #[serde(skip_serializing_if = "Option::is_none")]
864 pub name: Option<String>,
865 #[serde(skip_serializing_if = "Option::is_none")]
867 pub old_location: Option<Location>,
868 #[serde(skip_serializing_if = "Option::is_none")]
870 pub new_location: Option<Location>,
871 #[serde(skip_serializing_if = "Option::is_none")]
873 pub old_text: Option<String>,
874 #[serde(skip_serializing_if = "Option::is_none")]
876 pub new_text: Option<String>,
877 #[serde(skip_serializing_if = "Option::is_none")]
879 pub similarity: Option<f64>,
880 #[serde(skip_serializing_if = "Option::is_none")]
882 pub children: Option<Vec<ASTChange>>,
883 #[serde(skip_serializing_if = "Option::is_none")]
885 pub base_changes: Option<BaseChanges>,
886}
887
888#[derive(Debug, Clone, Default, Serialize, Deserialize)]
890pub struct DiffSummary {
891 pub total_changes: u32,
893 pub semantic_changes: u32,
895 pub inserts: u32,
897 pub deletes: u32,
899 pub updates: u32,
901 pub moves: u32,
903 pub renames: u32,
905 pub formats: u32,
907 pub extracts: u32,
909}
910
911#[derive(Debug, Clone, Serialize, Deserialize)]
921pub struct FileLevelChange {
922 pub relative_path: String,
924 pub change_type: ChangeType,
926 #[serde(skip_serializing_if = "Option::is_none")]
928 pub old_fingerprint: Option<u64>,
929 #[serde(skip_serializing_if = "Option::is_none")]
931 pub new_fingerprint: Option<u64>,
932 #[serde(skip_serializing_if = "Option::is_none")]
934 pub signature_changes: Option<Vec<String>>,
935}
936
937#[derive(Debug, Clone, Serialize, Deserialize)]
946pub struct ImportEdge {
947 pub source_file: String,
949 pub target_module: String,
951 pub imported_names: Vec<String>,
953}
954
955#[derive(Debug, Clone, Serialize, Deserialize)]
957pub struct ModuleLevelChange {
958 pub module_path: String,
960 pub change_type: ChangeType,
962 pub imports_added: Vec<ImportEdge>,
964 pub imports_removed: Vec<ImportEdge>,
966 #[serde(skip_serializing_if = "Option::is_none")]
968 pub file_change: Option<FileLevelChange>,
969}
970
971#[derive(Debug, Clone, Serialize, Deserialize)]
973pub struct ImportGraphSummary {
974 pub total_edges_a: usize,
976 pub total_edges_b: usize,
978 pub edges_added: usize,
980 pub edges_removed: usize,
982 pub modules_with_import_changes: usize,
984}
985
986#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
992pub enum ArchChangeType {
993 LayerMigration,
995 Added,
997 Removed,
999 CompositionChanged,
1001 CycleIntroduced,
1003 CycleResolved,
1005}
1006
1007#[derive(Debug, Clone, Serialize, Deserialize)]
1009pub struct ArchLevelChange {
1010 pub directory: String,
1012 pub change_type: ArchChangeType,
1014 #[serde(skip_serializing_if = "Option::is_none")]
1016 pub old_layer: Option<String>,
1017 #[serde(skip_serializing_if = "Option::is_none")]
1019 pub new_layer: Option<String>,
1020 #[serde(default)]
1022 pub migrated_functions: Vec<String>,
1023}
1024
1025#[derive(Debug, Clone, Serialize, Deserialize)]
1027pub struct ArchDiffSummary {
1028 pub layer_migrations: usize,
1030 pub directories_added: usize,
1032 pub directories_removed: usize,
1034 pub cycles_introduced: usize,
1036 pub cycles_resolved: usize,
1038 pub stability_score: f64,
1040}
1041
1042#[derive(Debug, Clone, Serialize, Deserialize)]
1048pub struct DiffReport {
1049 pub file_a: String,
1051 pub file_b: String,
1053 pub identical: bool,
1055 pub changes: Vec<ASTChange>,
1057 #[serde(skip_serializing_if = "Option::is_none")]
1059 pub summary: Option<DiffSummary>,
1060 #[serde(default)]
1062 pub granularity: DiffGranularity,
1063 #[serde(skip_serializing_if = "Option::is_none")]
1065 pub file_changes: Option<Vec<FileLevelChange>>,
1066 #[serde(skip_serializing_if = "Option::is_none")]
1068 pub module_changes: Option<Vec<ModuleLevelChange>>,
1069 #[serde(skip_serializing_if = "Option::is_none")]
1071 pub import_graph_summary: Option<ImportGraphSummary>,
1072 #[serde(skip_serializing_if = "Option::is_none")]
1074 pub arch_changes: Option<Vec<ArchLevelChange>>,
1075 #[serde(skip_serializing_if = "Option::is_none")]
1077 pub arch_summary: Option<ArchDiffSummary>,
1078}
1079
1080impl DiffReport {
1081 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#[derive(Debug, Clone, Serialize, Deserialize)]
1105pub struct ChangedFunction {
1106 pub name: String,
1108 pub file: String,
1110 pub line: u32,
1112 #[serde(default)]
1114 pub callers: Vec<CallInfo>,
1115}
1116
1117#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1119pub struct DiffImpactSummary {
1120 pub files_changed: u32,
1122 pub functions_changed: u32,
1124 pub tests_to_run: u32,
1126}
1127
1128#[derive(Debug, Clone, Serialize, Deserialize)]
1130pub struct DiffImpactReport {
1131 pub changed_functions: Vec<ChangedFunction>,
1133 pub suggested_tests: Vec<String>,
1135 pub summary: DiffImpactSummary,
1137}
1138
1139impl DiffImpactReport {
1140 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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
1185pub struct APIRule {
1186 pub id: String,
1188 pub name: String,
1190 pub category: MisuseCategory,
1192 pub severity: MisuseSeverity,
1194 pub description: String,
1196 pub correct_usage: String,
1198}
1199
1200#[derive(Debug, Clone, Serialize, Deserialize)]
1202pub struct MisuseFinding {
1203 pub file: String,
1205 pub line: u32,
1207 pub column: u32,
1209 pub rule: APIRule,
1211 pub api_call: String,
1213 pub message: String,
1215 pub fix_suggestion: String,
1217 #[serde(default)]
1219 pub code_context: String,
1220}
1221
1222#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1224pub struct APICheckSummary {
1225 pub total_findings: u32,
1227 #[serde(default)]
1229 pub by_category: HashMap<String, u32>,
1230 #[serde(default)]
1232 pub by_severity: HashMap<String, u32>,
1233 #[serde(default)]
1235 pub apis_checked: Vec<String>,
1236 pub files_scanned: u32,
1238}
1239
1240#[derive(Debug, Clone, Serialize, Deserialize)]
1242pub struct APICheckReport {
1243 pub findings: Vec<MisuseFinding>,
1245 pub summary: APICheckSummary,
1247 pub rules_applied: u32,
1249}
1250
1251impl APICheckReport {
1252 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#[derive(Debug, Clone, Serialize, Deserialize)]
1274pub struct ExpressionRef {
1275 pub text: String,
1277 pub line: u32,
1279 pub value_number: u32,
1281}
1282
1283#[derive(Debug, Clone, Serialize, Deserialize)]
1285pub struct GVNEquivalence {
1286 pub value_number: u32,
1288 pub expressions: Vec<ExpressionRef>,
1290 #[serde(default)]
1292 pub reason: String,
1293}
1294
1295#[derive(Debug, Clone, Serialize, Deserialize)]
1297pub struct Redundancy {
1298 pub original: ExpressionRef,
1300 pub redundant: ExpressionRef,
1302 #[serde(default)]
1304 pub reason: String,
1305}
1306
1307#[derive(Debug, Clone, Serialize, Deserialize)]
1309pub struct GVNSummary {
1310 pub total_expressions: u32,
1312 pub unique_values: u32,
1314 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#[derive(Debug, Clone, Serialize, Deserialize)]
1330pub struct GVNReport {
1331 pub function: String,
1333 #[serde(default)]
1335 pub equivalences: Vec<GVNEquivalence>,
1336 #[serde(default)]
1338 pub redundancies: Vec<Redundancy>,
1339 pub summary: GVNSummary,
1341}
1342
1343impl GVNReport {
1344 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#[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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
1420pub struct TaintFlow {
1421 pub file: String,
1423 pub line: u32,
1425 pub column: u32,
1427 pub code_snippet: String,
1429 pub description: String,
1431}
1432
1433#[derive(Debug, Clone, Serialize, Deserialize)]
1435pub struct VulnFinding {
1436 pub vuln_type: VulnType,
1438 pub severity: Severity,
1440 pub cwe_id: String,
1442 pub title: String,
1444 pub description: String,
1446 pub file: String,
1448 pub line: u32,
1450 pub column: u32,
1452 pub taint_flow: Vec<TaintFlow>,
1454 pub remediation: String,
1456 pub confidence: f64,
1458}
1459
1460#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1462pub struct VulnSummary {
1463 pub total_findings: u32,
1465 #[serde(default)]
1467 pub by_severity: HashMap<String, u32>,
1468 #[serde(default)]
1470 pub by_type: HashMap<String, u32>,
1471 pub files_with_vulns: u32,
1473}
1474
1475#[derive(Debug, Clone, Serialize, Deserialize)]
1477pub struct VulnReport {
1478 pub findings: Vec<VulnFinding>,
1480 #[serde(skip_serializing_if = "Option::is_none")]
1482 pub summary: Option<VulnSummary>,
1483 pub scan_duration_ms: u64,
1485 pub files_scanned: u32,
1487}
1488
1489impl VulnReport {
1490 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#[cfg(test)]
1512mod unit_types_tests {
1513 use super::*;
1514
1515 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}