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(
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 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#[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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct TodoItem {
139 pub category: String,
141 pub priority: u32,
143 pub description: String,
145 #[serde(default)]
147 pub file: String,
148 #[serde(default)]
150 pub line: u32,
151 #[serde(default)]
153 pub severity: String,
154 #[serde(default)]
156 pub score: f64,
157}
158
159impl TodoItem {
160 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 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 pub fn with_severity(mut self, severity: impl Into<String>) -> Self {
182 self.severity = severity.into();
183 self
184 }
185
186 pub fn with_score(mut self, score: f64) -> Self {
188 self.score = score;
189 self
190 }
191}
192
193#[derive(Debug, Clone, Default, Serialize, Deserialize)]
195pub struct TodoSummary {
196 pub dead_count: u32,
198 pub similar_pairs: u32,
200 pub low_cohesion_count: u32,
202 pub hotspot_count: u32,
204 pub equivalence_groups: u32,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct TodoReport {
211 pub wrapper: String,
213 pub path: String,
215 pub items: Vec<TodoItem>,
217 pub summary: TodoSummary,
219 #[serde(default)]
221 pub sub_results: HashMap<String, Value>,
222 pub total_elapsed_ms: f64,
224}
225
226impl TodoReport {
227 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#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct SecureFinding {
247 pub category: String,
249 pub severity: String,
251 pub description: String,
253 #[serde(default)]
255 pub file: String,
256 #[serde(default)]
258 pub line: u32,
259}
260
261impl SecureFinding {
262 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 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
287pub struct SecureSummary {
288 pub taint_count: u32,
290 pub taint_critical: u32,
292 pub leak_count: u32,
294 pub bounds_warnings: u32,
296 pub missing_contracts: u32,
298 pub mutable_params: u32,
300 #[serde(default)]
302 pub unsafe_blocks: u32,
303 #[serde(default)]
305 pub raw_pointer_ops: u32,
306 #[serde(default)]
308 pub unwrap_calls: u32,
309 #[serde(default)]
311 pub todo_markers: u32,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct SecureReport {
317 pub wrapper: String,
319 pub path: String,
321 pub findings: Vec<SecureFinding>,
323 pub summary: SecureSummary,
325 #[serde(default)]
327 pub sub_results: HashMap<String, Value>,
328 pub total_elapsed_ms: f64,
330}
331
332impl SecureReport {
333 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
442pub struct ParamInfo {
443 pub name: String,
445 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
447 pub type_hint: Option<String>,
448 #[serde(skip_serializing_if = "Option::is_none")]
450 pub default: Option<String>,
451}
452
453impl ParamInfo {
454 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 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 pub fn with_default(mut self, default: impl Into<String>) -> Self {
471 self.default = Some(default.into());
472 self
473 }
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize)]
478pub struct SignatureInfo {
479 pub params: Vec<ParamInfo>,
481 #[serde(skip_serializing_if = "Option::is_none")]
483 pub return_type: Option<String>,
484 #[serde(default)]
486 pub decorators: Vec<String>,
487 #[serde(default)]
489 pub is_async: bool,
490 #[serde(skip_serializing_if = "Option::is_none")]
492 pub docstring: Option<String>,
493}
494
495impl SignatureInfo {
496 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 pub fn with_param(mut self, param: ParamInfo) -> Self {
509 self.params.push(param);
510 self
511 }
512
513 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 pub fn with_docstring(mut self, docstring: impl Into<String>) -> Self {
521 self.docstring = Some(docstring.into());
522 self
523 }
524
525 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#[derive(Debug, Clone, Serialize, Deserialize)]
540pub struct PurityInfo {
541 pub classification: String,
543 #[serde(default)]
545 pub effects: Vec<String>,
546 pub confidence: String,
548}
549
550impl PurityInfo {
551 pub fn pure() -> Self {
553 Self {
554 classification: "pure".to_string(),
555 effects: Vec::new(),
556 confidence: "high".to_string(),
557 }
558 }
559
560 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 pub fn unknown() -> Self {
571 Self {
572 classification: "unknown".to_string(),
573 effects: Vec::new(),
574 confidence: "low".to_string(),
575 }
576 }
577
578 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#[derive(Debug, Clone, Serialize, Deserialize)]
593pub struct ComplexityInfo {
594 pub cyclomatic: u32,
596 pub num_blocks: u32,
598 pub num_edges: u32,
600 pub has_loops: bool,
602}
603
604impl ComplexityInfo {
605 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#[derive(Debug, Clone, Serialize, Deserialize)]
629pub struct CallInfo {
630 pub name: String,
632 pub file: String,
634 pub line: u32,
636}
637
638impl CallInfo {
639 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#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct ExplainReport {
652 pub function_name: String,
654 pub file: String,
656 pub line_start: u32,
658 pub line_end: u32,
660 pub language: String,
662 pub signature: SignatureInfo,
664 pub purity: PurityInfo,
666 #[serde(skip_serializing_if = "Option::is_none")]
668 pub complexity: Option<ComplexityInfo>,
669 #[serde(default)]
671 pub callers: Vec<CallInfo>,
672 #[serde(default)]
674 pub callees: Vec<CallInfo>,
675}
676
677impl ExplainReport {
678 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 pub fn with_signature(mut self, signature: SignatureInfo) -> Self {
702 self.signature = signature;
703 self
704 }
705
706 pub fn with_purity(mut self, purity: PurityInfo) -> Self {
708 self.purity = purity;
709 self
710 }
711
712 pub fn with_complexity(mut self, complexity: ComplexityInfo) -> Self {
714 self.complexity = Some(complexity);
715 self
716 }
717
718 pub fn add_caller(&mut self, caller: CallInfo) {
720 self.callers.push(caller);
721 }
722
723 pub fn add_callee(&mut self, callee: CallInfo) {
725 self.callees.push(callee);
726 }
727}
728
729#[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#[derive(Debug, Clone, Serialize, Deserialize)]
763pub struct SymbolInfo {
764 pub name: String,
766 pub kind: SymbolKind,
768 #[serde(skip_serializing_if = "Option::is_none")]
770 pub location: Option<Location>,
771 #[serde(skip_serializing_if = "Option::is_none")]
773 pub type_annotation: Option<String>,
774 #[serde(skip_serializing_if = "Option::is_none")]
776 pub docstring: Option<String>,
777 #[serde(default)]
779 pub is_builtin: bool,
780 #[serde(skip_serializing_if = "Option::is_none")]
782 pub module: Option<String>,
783}
784
785impl SymbolInfo {
786 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#[derive(Debug, Clone, Serialize, Deserialize)]
802pub struct DefinitionResult {
803 pub symbol: SymbolInfo,
805 #[serde(skip_serializing_if = "Option::is_none")]
807 pub definition: Option<Location>,
808 #[serde(skip_serializing_if = "Option::is_none")]
810 pub type_definition: Option<Location>,
811}
812
813#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum, Serialize, Deserialize)]
833#[serde(rename_all = "snake_case")]
834pub enum DiffGranularity {
835 Token,
837 Expression,
839 Statement,
841 #[default]
843 Function,
844 Class,
846 File,
848 Module,
850 Architecture,
852}
853
854#[derive(Debug, Clone, Serialize, Deserialize)]
856pub struct BaseChanges {
857 pub added: Vec<String>,
859 pub removed: Vec<String>,
861}
862
863#[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#[derive(Debug, Clone, Serialize, Deserialize)]
878pub struct ASTChange {
879 pub change_type: ChangeType,
881 pub node_kind: NodeKind,
883 #[serde(skip_serializing_if = "Option::is_none")]
885 pub name: Option<String>,
886 #[serde(skip_serializing_if = "Option::is_none")]
888 pub old_location: Option<Location>,
889 #[serde(skip_serializing_if = "Option::is_none")]
891 pub new_location: Option<Location>,
892 #[serde(skip_serializing_if = "Option::is_none")]
894 pub old_text: Option<String>,
895 #[serde(skip_serializing_if = "Option::is_none")]
897 pub new_text: Option<String>,
898 #[serde(skip_serializing_if = "Option::is_none")]
900 pub similarity: Option<f64>,
901 #[serde(skip_serializing_if = "Option::is_none")]
903 pub children: Option<Vec<ASTChange>>,
904 #[serde(skip_serializing_if = "Option::is_none")]
906 pub base_changes: Option<BaseChanges>,
907}
908
909#[derive(Debug, Clone, Default, Serialize, Deserialize)]
911pub struct DiffSummary {
912 pub total_changes: u32,
914 pub semantic_changes: u32,
916 pub inserts: u32,
918 pub deletes: u32,
920 pub updates: u32,
922 pub moves: u32,
924 pub renames: u32,
926 pub formats: u32,
928 pub extracts: u32,
930}
931
932#[derive(Debug, Clone, Serialize, Deserialize)]
942pub struct FileLevelChange {
943 pub relative_path: String,
945 pub change_type: ChangeType,
947 #[serde(skip_serializing_if = "Option::is_none")]
949 pub old_fingerprint: Option<u64>,
950 #[serde(skip_serializing_if = "Option::is_none")]
952 pub new_fingerprint: Option<u64>,
953 #[serde(skip_serializing_if = "Option::is_none")]
955 pub signature_changes: Option<Vec<String>>,
956}
957
958#[derive(Debug, Clone, Serialize, Deserialize)]
967pub struct ImportEdge {
968 pub source_file: String,
970 pub target_module: String,
972 pub imported_names: Vec<String>,
974}
975
976#[derive(Debug, Clone, Serialize, Deserialize)]
978pub struct ModuleLevelChange {
979 pub module_path: String,
981 pub change_type: ChangeType,
983 pub imports_added: Vec<ImportEdge>,
985 pub imports_removed: Vec<ImportEdge>,
987 #[serde(skip_serializing_if = "Option::is_none")]
989 pub file_change: Option<FileLevelChange>,
990}
991
992#[derive(Debug, Clone, Serialize, Deserialize)]
994pub struct ImportGraphSummary {
995 pub total_edges_a: usize,
997 pub total_edges_b: usize,
999 pub edges_added: usize,
1001 pub edges_removed: usize,
1003 pub modules_with_import_changes: usize,
1005}
1006
1007#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1013pub enum ArchChangeType {
1014 LayerMigration,
1016 Added,
1018 Removed,
1020 CompositionChanged,
1022 CycleIntroduced,
1024 CycleResolved,
1026}
1027
1028#[derive(Debug, Clone, Serialize, Deserialize)]
1030pub struct ArchLevelChange {
1031 pub directory: String,
1033 pub change_type: ArchChangeType,
1035 #[serde(skip_serializing_if = "Option::is_none")]
1037 pub old_layer: Option<String>,
1038 #[serde(skip_serializing_if = "Option::is_none")]
1040 pub new_layer: Option<String>,
1041 #[serde(default)]
1043 pub migrated_functions: Vec<String>,
1044}
1045
1046#[derive(Debug, Clone, Serialize, Deserialize)]
1048pub struct ArchDiffSummary {
1049 pub layer_migrations: usize,
1051 pub directories_added: usize,
1053 pub directories_removed: usize,
1055 pub cycles_introduced: usize,
1057 pub cycles_resolved: usize,
1059 pub stability_score: f64,
1061}
1062
1063#[derive(Debug, Clone, Serialize, Deserialize)]
1069pub struct DiffReport {
1070 pub file_a: String,
1072 pub file_b: String,
1074 pub identical: bool,
1076 pub changes: Vec<ASTChange>,
1078 #[serde(skip_serializing_if = "Option::is_none")]
1080 pub summary: Option<DiffSummary>,
1081 #[serde(default)]
1083 pub granularity: DiffGranularity,
1084 #[serde(skip_serializing_if = "Option::is_none")]
1086 pub file_changes: Option<Vec<FileLevelChange>>,
1087 #[serde(skip_serializing_if = "Option::is_none")]
1089 pub module_changes: Option<Vec<ModuleLevelChange>>,
1090 #[serde(skip_serializing_if = "Option::is_none")]
1092 pub import_graph_summary: Option<ImportGraphSummary>,
1093 #[serde(skip_serializing_if = "Option::is_none")]
1095 pub arch_changes: Option<Vec<ArchLevelChange>>,
1096 #[serde(skip_serializing_if = "Option::is_none")]
1098 pub arch_summary: Option<ArchDiffSummary>,
1099}
1100
1101impl DiffReport {
1102 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#[derive(Debug, Clone, Serialize, Deserialize)]
1126pub struct ChangedFunction {
1127 pub name: String,
1129 pub file: String,
1131 pub line: u32,
1133 #[serde(default)]
1135 pub callers: Vec<CallInfo>,
1136}
1137
1138#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1140pub struct DiffImpactSummary {
1141 pub files_changed: u32,
1143 pub functions_changed: u32,
1145 pub tests_to_run: u32,
1147}
1148
1149#[derive(Debug, Clone, Serialize, Deserialize)]
1151pub struct DiffImpactReport {
1152 pub changed_functions: Vec<ChangedFunction>,
1154 pub suggested_tests: Vec<String>,
1156 pub summary: DiffImpactSummary,
1158}
1159
1160impl DiffImpactReport {
1161 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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
1206pub struct APIRule {
1207 pub id: String,
1209 pub name: String,
1211 pub category: MisuseCategory,
1213 pub severity: MisuseSeverity,
1215 pub description: String,
1217 pub correct_usage: String,
1219}
1220
1221#[derive(Debug, Clone, Serialize, Deserialize)]
1223pub struct MisuseFinding {
1224 pub file: String,
1226 pub line: u32,
1228 pub column: u32,
1230 pub rule: APIRule,
1232 pub api_call: String,
1234 pub message: String,
1236 pub fix_suggestion: String,
1238 #[serde(default)]
1240 pub code_context: String,
1241}
1242
1243#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1245pub struct APICheckSummary {
1246 pub total_findings: u32,
1248 #[serde(default)]
1250 pub by_category: HashMap<String, u32>,
1251 #[serde(default)]
1253 pub by_severity: HashMap<String, u32>,
1254 #[serde(default)]
1256 pub apis_checked: Vec<String>,
1257 pub files_scanned: u32,
1259}
1260
1261#[derive(Debug, Clone, Serialize, Deserialize)]
1263pub struct APICheckReport {
1264 pub findings: Vec<MisuseFinding>,
1266 pub summary: APICheckSummary,
1268 pub rules_applied: u32,
1270}
1271
1272impl APICheckReport {
1273 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#[derive(Debug, Clone, Serialize, Deserialize)]
1295pub struct ExpressionRef {
1296 pub text: String,
1298 pub line: u32,
1300 pub value_number: u32,
1302}
1303
1304#[derive(Debug, Clone, Serialize, Deserialize)]
1306pub struct GVNEquivalence {
1307 pub value_number: u32,
1309 pub expressions: Vec<ExpressionRef>,
1311 #[serde(default)]
1313 pub reason: String,
1314}
1315
1316#[derive(Debug, Clone, Serialize, Deserialize)]
1318pub struct Redundancy {
1319 pub original: ExpressionRef,
1321 pub redundant: ExpressionRef,
1323 #[serde(default)]
1325 pub reason: String,
1326}
1327
1328#[derive(Debug, Clone, Serialize, Deserialize)]
1330pub struct GVNSummary {
1331 pub total_expressions: u32,
1333 pub unique_values: u32,
1335 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#[derive(Debug, Clone, Serialize, Deserialize)]
1351pub struct GVNReport {
1352 pub function: String,
1354 #[serde(default)]
1356 pub equivalences: Vec<GVNEquivalence>,
1357 #[serde(default)]
1359 pub redundancies: Vec<Redundancy>,
1360 pub summary: GVNSummary,
1362}
1363
1364impl GVNReport {
1365 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#[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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
1441pub struct TaintFlow {
1442 pub file: String,
1444 pub line: u32,
1446 pub column: u32,
1448 pub code_snippet: String,
1450 pub description: String,
1452}
1453
1454#[derive(Debug, Clone, Serialize, Deserialize)]
1456pub struct VulnFinding {
1457 pub vuln_type: VulnType,
1459 pub severity: Severity,
1461 pub cwe_id: String,
1463 pub title: String,
1465 pub description: String,
1467 pub file: String,
1469 pub line: u32,
1471 pub column: u32,
1473 pub taint_flow: Vec<TaintFlow>,
1475 pub remediation: String,
1477 pub confidence: f64,
1479}
1480
1481#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1483pub struct VulnSummary {
1484 pub total_findings: u32,
1486 #[serde(default)]
1488 pub by_severity: HashMap<String, u32>,
1489 #[serde(default)]
1491 pub by_type: HashMap<String, u32>,
1492 pub files_with_vulns: u32,
1494}
1495
1496#[derive(Debug, Clone, Serialize, Deserialize)]
1498pub struct VulnReport {
1499 pub findings: Vec<VulnFinding>,
1501 #[serde(skip_serializing_if = "Option::is_none")]
1503 pub summary: Option<VulnSummary>,
1504 pub scan_duration_ms: u64,
1506 pub files_scanned: u32,
1508}
1509
1510impl VulnReport {
1511 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#[cfg(test)]
1533mod unit_types_tests {
1534 use super::*;
1535
1536 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}