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, skip_serializing_if = "HashMap::is_empty")]
216 pub sub_results: HashMap<String, Value>,
217 pub total_elapsed_ms: f64,
219}
220
221impl TodoReport {
222 pub fn new(path: impl Into<String>) -> Self {
224 Self {
225 wrapper: "todo".to_string(),
226 path: path.into(),
227 items: Vec::new(),
228 summary: TodoSummary::default(),
229 sub_results: HashMap::new(),
230 total_elapsed_ms: 0.0,
231 }
232 }
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct SecureFinding {
242 pub category: String,
244 pub severity: String,
246 pub description: String,
248 #[serde(default)]
250 pub file: String,
251 #[serde(default)]
253 pub line: u32,
254}
255
256impl SecureFinding {
257 pub fn new(
259 category: impl Into<String>,
260 severity: impl Into<String>,
261 description: impl Into<String>,
262 ) -> Self {
263 Self {
264 category: category.into(),
265 severity: severity.into(),
266 description: description.into(),
267 file: String::new(),
268 line: 0,
269 }
270 }
271
272 pub fn with_location(mut self, file: impl Into<String>, line: u32) -> Self {
274 self.file = file.into();
275 self.line = line;
276 self
277 }
278}
279
280#[derive(Debug, Clone, Default, Serialize, Deserialize)]
290pub struct SecureSummary {
291 pub taint_count: u32,
293 pub taint_critical: u32,
295 pub leak_count: u32,
297 pub bounds_warnings: u32,
299 #[serde(default)]
306 pub behavioral_count: u32,
307 pub missing_contracts: u32,
309 pub mutable_params: u32,
311 #[serde(default)]
313 pub unsafe_blocks: u32,
314 #[serde(default)]
316 pub raw_pointer_ops: u32,
317 #[serde(default)]
319 pub unwrap_calls: u32,
320 #[serde(default)]
322 pub todo_markers: u32,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct SecureReport {
328 pub wrapper: String,
330 #[serde(rename = "root", alias = "path")]
337 pub path: String,
338 pub findings: Vec<SecureFinding>,
340 pub summary: SecureSummary,
342 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
352 pub sub_results: HashMap<String, Value>,
353 pub total_elapsed_ms: f64,
355 #[serde(default, skip_serializing_if = "is_zero_u32")]
366 pub files_skipped: u32,
367 #[serde(default, skip_serializing_if = "Vec::is_empty")]
373 pub warnings: Vec<String>,
374}
375
376fn is_zero_u32(n: &u32) -> bool {
379 *n == 0
380}
381
382impl SecureReport {
383 pub fn new(path: impl Into<String>) -> Self {
385 Self {
386 wrapper: "secure".to_string(),
387 path: path.into(),
388 findings: Vec::new(),
389 summary: SecureSummary::default(),
390 sub_results: HashMap::new(),
391 total_elapsed_ms: 0.0,
392 files_skipped: 0,
393 warnings: Vec::new(),
394 }
395 }
396}
397
398#[cfg(test)]
403mod tests {
404 use super::*;
405
406 #[test]
407 fn test_output_format_serialization() {
408 let json = serde_json::to_string(&OutputFormat::Json).unwrap();
409 assert_eq!(json, r#""json""#);
410
411 let text = serde_json::to_string(&OutputFormat::Text).unwrap();
412 assert_eq!(text, r#""text""#);
413
414 let sarif = serde_json::to_string(&OutputFormat::Sarif).unwrap();
415 assert_eq!(sarif, r#""sarif""#);
416 }
417
418 #[test]
419 fn test_severity_ordering() {
420 assert!(Severity::Critical.order() < Severity::High.order());
421 assert!(Severity::High.order() < Severity::Medium.order());
422 assert!(Severity::Medium.order() < Severity::Low.order());
423 assert!(Severity::Low.order() < Severity::Info.order());
424 }
425
426 #[test]
427 fn test_location_serialization() {
428 let loc = Location::new("test.py", 42);
429 let json = serde_json::to_string(&loc).unwrap();
430 assert!(json.contains(r#""file":"test.py""#));
431 assert!(json.contains(r#""line":42"#));
432 }
433
434 #[test]
435 fn test_todo_report_serialization() {
436 let mut report = TodoReport::new("/path/to/file.py");
437 report
438 .items
439 .push(TodoItem::new("dead_code", 1, "Unused function"));
440 report.summary.dead_count = 1;
441
442 let json = serde_json::to_string(&report).unwrap();
443 assert!(json.contains(r#""wrapper":"todo""#));
444 assert!(json.contains(r#""dead_count":1"#));
445 }
446
447 #[test]
448 fn test_todo_item_builder() {
449 let item = TodoItem::new("complexity", 2, "High cyclomatic complexity")
450 .with_location("src/main.py", 100)
451 .with_severity("high")
452 .with_score(0.85);
453
454 assert_eq!(item.category, "complexity");
455 assert_eq!(item.file, "src/main.py");
456 assert_eq!(item.line, 100);
457 assert_eq!(item.severity, "high");
458 assert!((item.score - 0.85).abs() < 0.001);
459 }
460
461 #[test]
462 fn test_secure_report_serialization() {
463 let mut report = SecureReport::new("/path/to/file.py");
464 report
465 .findings
466 .push(SecureFinding::new("taint", "critical", "SQL injection"));
467 report.summary.taint_count = 1;
468 report.summary.taint_critical = 1;
469
470 let json = serde_json::to_string(&report).unwrap();
471 assert!(json.contains(r#""wrapper":"secure""#));
472 assert!(json.contains(r#""taint_count":1"#));
473 assert!(json.contains(r#""taint_critical":1"#));
474 }
475
476 #[test]
477 fn test_secure_finding_builder() {
478 let finding = SecureFinding::new("resource_leak", "high", "File not closed")
479 .with_location("src/db.py", 42);
480
481 assert_eq!(finding.category, "resource_leak");
482 assert_eq!(finding.severity, "high");
483 assert_eq!(finding.file, "src/db.py");
484 assert_eq!(finding.line, 42);
485 }
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
494pub struct ParamInfo {
495 pub name: String,
497 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
499 pub type_hint: Option<String>,
500 #[serde(skip_serializing_if = "Option::is_none")]
502 pub default: Option<String>,
503}
504
505impl ParamInfo {
506 pub fn new(name: impl Into<String>) -> Self {
508 Self {
509 name: name.into(),
510 type_hint: None,
511 default: None,
512 }
513 }
514
515 pub fn with_type(mut self, type_hint: impl Into<String>) -> Self {
517 self.type_hint = Some(type_hint.into());
518 self
519 }
520
521 pub fn with_default(mut self, default: impl Into<String>) -> Self {
523 self.default = Some(default.into());
524 self
525 }
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize)]
530pub struct SignatureInfo {
531 pub params: Vec<ParamInfo>,
533 #[serde(skip_serializing_if = "Option::is_none")]
535 pub return_type: Option<String>,
536 #[serde(default)]
538 pub decorators: Vec<String>,
539 #[serde(default)]
541 pub is_async: bool,
542 #[serde(skip_serializing_if = "Option::is_none")]
544 pub docstring: Option<String>,
545}
546
547impl SignatureInfo {
548 pub fn new() -> Self {
550 Self {
551 params: Vec::new(),
552 return_type: None,
553 decorators: Vec::new(),
554 is_async: false,
555 docstring: None,
556 }
557 }
558
559 pub fn with_param(mut self, param: ParamInfo) -> Self {
561 self.params.push(param);
562 self
563 }
564
565 pub fn with_return_type(mut self, return_type: impl Into<String>) -> Self {
567 self.return_type = Some(return_type.into());
568 self
569 }
570
571 pub fn with_docstring(mut self, docstring: impl Into<String>) -> Self {
573 self.docstring = Some(docstring.into());
574 self
575 }
576
577 pub fn set_async(mut self, is_async: bool) -> Self {
579 self.is_async = is_async;
580 self
581 }
582}
583
584impl Default for SignatureInfo {
585 fn default() -> Self {
586 Self::new()
587 }
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct PurityInfo {
593 pub classification: String,
595 #[serde(default)]
597 pub effects: Vec<String>,
598 pub confidence: String,
600}
601
602impl PurityInfo {
603 pub fn pure() -> Self {
605 Self {
606 classification: "pure".to_string(),
607 effects: Vec::new(),
608 confidence: "high".to_string(),
609 }
610 }
611
612 pub fn impure(effects: Vec<String>) -> Self {
614 Self {
615 classification: "impure".to_string(),
616 effects,
617 confidence: "high".to_string(),
618 }
619 }
620
621 pub fn unknown() -> Self {
623 Self {
624 classification: "unknown".to_string(),
625 effects: Vec::new(),
626 confidence: "low".to_string(),
627 }
628 }
629
630 pub fn with_confidence(mut self, confidence: impl Into<String>) -> Self {
632 self.confidence = confidence.into();
633 self
634 }
635}
636
637impl Default for PurityInfo {
638 fn default() -> Self {
639 Self::unknown()
640 }
641}
642
643#[derive(Debug, Clone, Serialize, Deserialize)]
645pub struct ComplexityInfo {
646 pub cyclomatic: u32,
648 pub num_blocks: u32,
650 pub num_edges: u32,
652 pub has_loops: bool,
654}
655
656impl ComplexityInfo {
657 pub fn new(cyclomatic: u32, num_blocks: u32, num_edges: u32, has_loops: bool) -> Self {
659 Self {
660 cyclomatic,
661 num_blocks,
662 num_edges,
663 has_loops,
664 }
665 }
666}
667
668impl Default for ComplexityInfo {
669 fn default() -> Self {
670 Self {
671 cyclomatic: 1,
672 num_blocks: 1,
673 num_edges: 0,
674 has_loops: false,
675 }
676 }
677}
678
679#[derive(Debug, Clone, Serialize, Deserialize)]
681pub struct CallInfo {
682 pub name: String,
684 pub file: String,
686 pub line: u32,
688}
689
690impl CallInfo {
691 pub fn new(name: impl Into<String>, file: impl Into<String>, line: u32) -> Self {
693 Self {
694 name: name.into(),
695 file: file.into(),
696 line,
697 }
698 }
699}
700
701#[derive(Debug, Clone, Deserialize)]
708pub struct ExplainReport {
709 #[serde(alias = "function")]
717 pub function_name: String,
718 pub file: String,
720 pub line_start: u32,
722 pub line_end: u32,
724 pub language: String,
726 pub signature: SignatureInfo,
728 pub purity: PurityInfo,
730 #[serde(skip_serializing_if = "Option::is_none")]
732 pub complexity: Option<ComplexityInfo>,
733 #[serde(default)]
735 pub callers: Vec<CallInfo>,
736 #[serde(default)]
738 pub callees: Vec<CallInfo>,
739}
740
741impl Serialize for ExplainReport {
744 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
745 where
746 S: serde::Serializer,
747 {
748 use serde::ser::SerializeStruct;
749 let mut count = 8; if self.complexity.is_some() {
751 count += 1;
752 }
753 count += 2;
756 let mut s = serializer.serialize_struct("ExplainReport", count)?;
757 s.serialize_field("function", &self.function_name)?;
760 s.serialize_field("file", &self.file)?;
761 s.serialize_field("line_start", &self.line_start)?;
762 s.serialize_field("line_end", &self.line_end)?;
763 s.serialize_field("line", &self.line_start)?;
764 s.serialize_field("language", &self.language)?;
765 s.serialize_field("signature", &self.signature)?;
766 s.serialize_field("purity", &self.purity)?;
767 if let Some(c) = &self.complexity {
768 s.serialize_field("complexity", c)?;
769 }
770 s.serialize_field("callers", &self.callers)?;
771 s.serialize_field("callees", &self.callees)?;
772 s.end()
773 }
774}
775
776impl ExplainReport {
777 pub fn new(
779 function_name: impl Into<String>,
780 file: impl Into<String>,
781 line_start: u32,
782 line_end: u32,
783 language: impl Into<String>,
784 ) -> Self {
785 Self {
786 function_name: function_name.into(),
787 file: file.into(),
788 line_start,
789 line_end,
790 language: language.into(),
791 signature: SignatureInfo::default(),
792 purity: PurityInfo::default(),
793 complexity: None,
794 callers: Vec::new(),
795 callees: Vec::new(),
796 }
797 }
798
799 pub fn with_signature(mut self, signature: SignatureInfo) -> Self {
801 self.signature = signature;
802 self
803 }
804
805 pub fn with_purity(mut self, purity: PurityInfo) -> Self {
807 self.purity = purity;
808 self
809 }
810
811 pub fn with_complexity(mut self, complexity: ComplexityInfo) -> Self {
813 self.complexity = Some(complexity);
814 self
815 }
816
817 pub fn add_caller(&mut self, caller: CallInfo) {
819 self.callers.push(caller);
820 }
821
822 pub fn add_callee(&mut self, callee: CallInfo) {
824 self.callees.push(callee);
825 }
826}
827
828#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum, Default)]
834#[serde(rename_all = "snake_case")]
835pub enum SymbolKind {
836 Function,
837 Class,
838 Method,
839 Variable,
840 Parameter,
841 Constant,
842 Module,
843 Type,
844 Interface,
845 Property,
846 #[default]
847 Unknown,
848}
849
850#[derive(Debug, Clone, Serialize, Deserialize)]
852pub struct SymbolInfo {
853 pub name: String,
855 pub kind: SymbolKind,
857 #[serde(skip_serializing_if = "Option::is_none")]
859 pub location: Option<Location>,
860 #[serde(skip_serializing_if = "Option::is_none")]
862 pub type_annotation: Option<String>,
863 #[serde(skip_serializing_if = "Option::is_none")]
865 pub docstring: Option<String>,
866 #[serde(default)]
868 pub is_builtin: bool,
869 #[serde(skip_serializing_if = "Option::is_none")]
871 pub module: Option<String>,
872}
873
874impl SymbolInfo {
875 pub fn new(name: impl Into<String>, kind: SymbolKind) -> Self {
877 Self {
878 name: name.into(),
879 kind,
880 location: None,
881 type_annotation: None,
882 docstring: None,
883 is_builtin: false,
884 module: None,
885 }
886 }
887}
888
889#[derive(Debug, Clone, Serialize, Deserialize)]
891pub struct DefinitionResult {
892 pub symbol: SymbolInfo,
894 #[serde(skip_serializing_if = "Option::is_none")]
896 pub definition: Option<Location>,
897 #[serde(skip_serializing_if = "Option::is_none")]
899 pub type_definition: Option<Location>,
900}
901
902#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
908#[serde(rename_all = "snake_case")]
909pub enum ChangeType {
910 Insert,
911 Delete,
912 Update,
913 Move,
914 Rename,
915 Extract,
916 Inline,
917 Format,
918}
919
920#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum, Serialize, Deserialize)]
922#[serde(rename_all = "snake_case")]
923pub enum DiffGranularity {
924 Token,
926 Expression,
928 Statement,
930 #[default]
932 Function,
933 Class,
935 File,
937 Module,
939 Architecture,
941}
942
943#[derive(Debug, Clone, Serialize, Deserialize)]
945pub struct BaseChanges {
946 pub added: Vec<String>,
948 pub removed: Vec<String>,
950}
951
952#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
954#[serde(rename_all = "snake_case")]
955pub enum NodeKind {
956 Function,
957 Class,
958 Method,
959 Field,
960 Statement,
961 Expression,
962 Block,
963}
964
965#[derive(Debug, Clone, Serialize, Deserialize)]
967pub struct ASTChange {
968 pub change_type: ChangeType,
970 pub node_kind: NodeKind,
972 #[serde(skip_serializing_if = "Option::is_none")]
974 pub name: Option<String>,
975 #[serde(skip_serializing_if = "Option::is_none")]
977 pub old_location: Option<Location>,
978 #[serde(skip_serializing_if = "Option::is_none")]
980 pub new_location: Option<Location>,
981 #[serde(skip_serializing_if = "Option::is_none")]
983 pub old_text: Option<String>,
984 #[serde(skip_serializing_if = "Option::is_none")]
986 pub new_text: Option<String>,
987 #[serde(skip_serializing_if = "Option::is_none")]
989 pub similarity: Option<f64>,
990 #[serde(skip_serializing_if = "Option::is_none")]
992 pub children: Option<Vec<ASTChange>>,
993 #[serde(skip_serializing_if = "Option::is_none")]
995 pub base_changes: Option<BaseChanges>,
996}
997
998#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1000pub struct DiffSummary {
1001 pub total_changes: u32,
1003 pub semantic_changes: u32,
1005 pub inserts: u32,
1007 pub deletes: u32,
1009 pub updates: u32,
1011 pub moves: u32,
1013 pub renames: u32,
1015 pub formats: u32,
1017 pub extracts: u32,
1019}
1020
1021#[derive(Debug, Clone, Serialize, Deserialize)]
1031pub struct FileLevelChange {
1032 pub relative_path: String,
1034 pub change_type: ChangeType,
1036 #[serde(skip_serializing_if = "Option::is_none")]
1038 pub old_fingerprint: Option<u64>,
1039 #[serde(skip_serializing_if = "Option::is_none")]
1041 pub new_fingerprint: Option<u64>,
1042 #[serde(skip_serializing_if = "Option::is_none")]
1044 pub signature_changes: Option<Vec<String>>,
1045}
1046
1047#[derive(Debug, Clone, Serialize, Deserialize)]
1056pub struct ImportEdge {
1057 pub source_file: String,
1059 pub target_module: String,
1061 pub imported_names: Vec<String>,
1063}
1064
1065#[derive(Debug, Clone, Serialize, Deserialize)]
1067pub struct ModuleLevelChange {
1068 pub module_path: String,
1070 pub change_type: ChangeType,
1072 pub imports_added: Vec<ImportEdge>,
1074 pub imports_removed: Vec<ImportEdge>,
1076 #[serde(skip_serializing_if = "Option::is_none")]
1078 pub file_change: Option<FileLevelChange>,
1079}
1080
1081#[derive(Debug, Clone, Serialize, Deserialize)]
1083pub struct ImportGraphSummary {
1084 pub total_edges_a: usize,
1086 pub total_edges_b: usize,
1088 pub edges_added: usize,
1090 pub edges_removed: usize,
1092 pub modules_with_import_changes: usize,
1094}
1095
1096#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1102pub enum ArchChangeType {
1103 LayerMigration,
1105 Added,
1107 Removed,
1109 CompositionChanged,
1111 CycleIntroduced,
1113 CycleResolved,
1115}
1116
1117#[derive(Debug, Clone, Serialize, Deserialize)]
1119pub struct ArchLevelChange {
1120 pub directory: String,
1122 pub change_type: ArchChangeType,
1124 #[serde(skip_serializing_if = "Option::is_none")]
1126 pub old_layer: Option<String>,
1127 #[serde(skip_serializing_if = "Option::is_none")]
1129 pub new_layer: Option<String>,
1130 #[serde(default)]
1132 pub migrated_functions: Vec<String>,
1133}
1134
1135#[derive(Debug, Clone, Serialize, Deserialize)]
1137pub struct ArchDiffSummary {
1138 pub layer_migrations: usize,
1140 pub directories_added: usize,
1142 pub directories_removed: usize,
1144 pub cycles_introduced: usize,
1146 pub cycles_resolved: usize,
1148 pub stability_score: f64,
1150}
1151
1152#[derive(Debug, Clone, Serialize, Deserialize)]
1158pub struct DiffReport {
1159 pub file_a: String,
1161 pub file_b: String,
1163 pub identical: bool,
1165 pub changes: Vec<ASTChange>,
1167 #[serde(skip_serializing_if = "Option::is_none")]
1169 pub summary: Option<DiffSummary>,
1170 #[serde(default)]
1172 pub granularity: DiffGranularity,
1173 #[serde(skip_serializing_if = "Option::is_none")]
1175 pub file_changes: Option<Vec<FileLevelChange>>,
1176 #[serde(skip_serializing_if = "Option::is_none")]
1178 pub module_changes: Option<Vec<ModuleLevelChange>>,
1179 #[serde(skip_serializing_if = "Option::is_none")]
1181 pub import_graph_summary: Option<ImportGraphSummary>,
1182 #[serde(skip_serializing_if = "Option::is_none")]
1184 pub arch_changes: Option<Vec<ArchLevelChange>>,
1185 #[serde(skip_serializing_if = "Option::is_none")]
1187 pub arch_summary: Option<ArchDiffSummary>,
1188}
1189
1190impl DiffReport {
1191 pub fn new(file_a: impl Into<String>, file_b: impl Into<String>) -> Self {
1193 Self {
1194 file_a: file_a.into(),
1195 file_b: file_b.into(),
1196 identical: true,
1197 changes: Vec::new(),
1198 summary: Some(DiffSummary::default()),
1199 granularity: DiffGranularity::Function,
1200 file_changes: None,
1201 module_changes: None,
1202 import_graph_summary: None,
1203 arch_changes: None,
1204 arch_summary: None,
1205 }
1206 }
1207}
1208
1209#[derive(Debug, Clone, Serialize, Deserialize)]
1215pub struct ChangedFunction {
1216 pub name: String,
1218 pub file: String,
1220 pub line: u32,
1222 #[serde(default)]
1224 pub callers: Vec<CallInfo>,
1225}
1226
1227#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1229pub struct DiffImpactSummary {
1230 pub files_changed: u32,
1232 pub functions_changed: u32,
1234 pub tests_to_run: u32,
1236}
1237
1238#[derive(Debug, Clone, Serialize, Deserialize)]
1240pub struct DiffImpactReport {
1241 pub changed_functions: Vec<ChangedFunction>,
1243 pub suggested_tests: Vec<String>,
1245 pub summary: DiffImpactSummary,
1247}
1248
1249impl DiffImpactReport {
1250 pub fn new() -> Self {
1252 Self {
1253 changed_functions: Vec::new(),
1254 suggested_tests: Vec::new(),
1255 summary: DiffImpactSummary::default(),
1256 }
1257 }
1258}
1259
1260impl Default for DiffImpactReport {
1261 fn default() -> Self {
1262 Self::new()
1263 }
1264}
1265
1266#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
1272#[serde(rename_all = "snake_case")]
1273pub enum MisuseCategory {
1274 CallOrder,
1275 ErrorHandling,
1276 Parameters,
1277 Resources,
1278 Crypto,
1279 Concurrency,
1280 Security,
1281}
1282
1283#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
1285#[serde(rename_all = "snake_case")]
1286pub enum MisuseSeverity {
1287 Info,
1288 Low,
1289 Medium,
1290 High,
1291}
1292
1293#[derive(Debug, Clone, Serialize, Deserialize)]
1295pub struct APIRule {
1296 pub id: String,
1298 pub name: String,
1300 pub category: MisuseCategory,
1302 pub severity: MisuseSeverity,
1304 pub description: String,
1306 pub correct_usage: String,
1308}
1309
1310#[derive(Debug, Clone, Serialize, Deserialize)]
1312pub struct MisuseFinding {
1313 pub file: String,
1315 pub line: u32,
1317 pub column: u32,
1319 pub rule: APIRule,
1321 pub api_call: String,
1323 pub message: String,
1325 pub fix_suggestion: String,
1327 #[serde(default)]
1329 pub code_context: String,
1330}
1331
1332#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1334pub struct APICheckSummary {
1335 pub total_findings: u32,
1337 #[serde(default)]
1339 pub by_category: HashMap<String, u32>,
1340 #[serde(default)]
1342 pub by_severity: HashMap<String, u32>,
1343 #[serde(default)]
1345 pub apis_checked: Vec<String>,
1346 pub files_scanned: u32,
1348}
1349
1350#[derive(Debug, Clone, Deserialize)]
1352pub struct APICheckReport {
1353 pub findings: Vec<MisuseFinding>,
1355 pub summary: APICheckSummary,
1357 pub rules_applied: u32,
1359}
1360
1361impl Serialize for APICheckReport {
1368 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1369 where
1370 S: serde::Serializer,
1371 {
1372 use serde::ser::SerializeStruct;
1373 let mut state = serializer.serialize_struct("APICheckReport", 5)?;
1374 state.serialize_field("findings", &self.findings)?;
1375 state.serialize_field("summary", &self.summary)?;
1376 state.serialize_field("rules_applied", &self.rules_applied)?;
1377 state.serialize_field("total_findings", &self.summary.total_findings)?;
1379 state.serialize_field("files_scanned", &self.summary.files_scanned)?;
1380 state.end()
1381 }
1382}
1383
1384impl APICheckReport {
1385 pub fn new() -> Self {
1387 Self {
1388 findings: Vec::new(),
1389 summary: APICheckSummary::default(),
1390 rules_applied: 0,
1391 }
1392 }
1393}
1394
1395impl Default for APICheckReport {
1396 fn default() -> Self {
1397 Self::new()
1398 }
1399}
1400
1401#[derive(Debug, Clone, Serialize, Deserialize)]
1407pub struct ExpressionRef {
1408 pub text: String,
1410 pub line: u32,
1412 pub value_number: u32,
1414}
1415
1416#[derive(Debug, Clone, Serialize, Deserialize)]
1418pub struct GVNEquivalence {
1419 pub value_number: u32,
1421 pub expressions: Vec<ExpressionRef>,
1423 #[serde(default)]
1425 pub reason: String,
1426}
1427
1428#[derive(Debug, Clone, Serialize, Deserialize)]
1430pub struct Redundancy {
1431 pub original: ExpressionRef,
1433 pub redundant: ExpressionRef,
1435 #[serde(default)]
1437 pub reason: String,
1438}
1439
1440#[derive(Debug, Clone, Serialize, Deserialize)]
1442pub struct GVNSummary {
1443 pub total_expressions: u32,
1445 pub unique_values: u32,
1447 pub compression_ratio: f64,
1449}
1450
1451impl Default for GVNSummary {
1452 fn default() -> Self {
1453 Self {
1454 total_expressions: 0,
1455 unique_values: 0,
1456 compression_ratio: 1.0,
1457 }
1458 }
1459}
1460
1461#[derive(Debug, Clone, Serialize, Deserialize)]
1463pub struct GVNReport {
1464 pub function: String,
1466 #[serde(default)]
1468 pub equivalences: Vec<GVNEquivalence>,
1469 #[serde(default)]
1471 pub redundancies: Vec<Redundancy>,
1472 pub summary: GVNSummary,
1474}
1475
1476impl GVNReport {
1477 pub fn new(function: impl Into<String>) -> Self {
1479 Self {
1480 function: function.into(),
1481 equivalences: Vec::new(),
1482 redundancies: Vec::new(),
1483 summary: GVNSummary::default(),
1484 }
1485 }
1486}
1487
1488#[derive(
1501 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, ValueEnum,
1502)]
1503#[serde(rename_all = "snake_case")]
1504#[value(rename_all = "snake_case")]
1505pub enum VulnType {
1506 SqlInjection,
1507 Xss,
1508 CommandInjection,
1509 Ssrf,
1510 PathTraversal,
1511 Deserialization,
1512 UnsafeCode,
1513 MemorySafety,
1514 Panic,
1515 Xxe,
1516 OpenRedirect,
1517 LdapInjection,
1518 XpathInjection,
1519}
1520
1521impl VulnType {
1522 pub fn cwe_id(&self) -> &'static str {
1524 match self {
1525 Self::SqlInjection => "CWE-89",
1526 Self::Xss => "CWE-79",
1527 Self::CommandInjection => "CWE-78",
1528 Self::Ssrf => "CWE-918",
1529 Self::PathTraversal => "CWE-22",
1530 Self::Deserialization => "CWE-502",
1531 Self::UnsafeCode => "CWE-242",
1532 Self::MemorySafety => "CWE-119",
1533 Self::Panic => "CWE-703",
1534 Self::Xxe => "CWE-611",
1535 Self::OpenRedirect => "CWE-601",
1536 Self::LdapInjection => "CWE-90",
1537 Self::XpathInjection => "CWE-643",
1538 }
1539 }
1540
1541 pub fn default_severity(&self) -> Severity {
1543 match self {
1544 Self::SqlInjection
1545 | Self::CommandInjection
1546 | Self::Deserialization
1547 | Self::MemorySafety => Severity::Critical,
1548 Self::Xxe
1549 | Self::Xss
1550 | Self::Ssrf
1551 | Self::PathTraversal
1552 | Self::LdapInjection
1553 | Self::XpathInjection
1554 | Self::UnsafeCode => Severity::High,
1555 Self::OpenRedirect | Self::Panic => Severity::Medium,
1556 }
1557 }
1558}
1559
1560#[derive(Debug, Clone, Serialize, Deserialize)]
1562pub struct TaintFlow {
1563 pub file: String,
1565 pub line: u32,
1567 pub column: u32,
1569 pub code_snippet: String,
1571 pub description: String,
1573}
1574
1575#[derive(Debug, Clone, Serialize, Deserialize)]
1577pub struct VulnFinding {
1578 pub vuln_type: VulnType,
1580 pub severity: Severity,
1582 pub cwe_id: String,
1584 pub title: String,
1586 pub description: String,
1588 pub file: String,
1590 pub line: u32,
1592 pub column: u32,
1594 #[serde(default, skip_serializing_if = "Option::is_none")]
1606 pub function: Option<String>,
1607 pub taint_flow: Vec<TaintFlow>,
1611 pub remediation: String,
1613 pub confidence: f64,
1615 #[serde(default, skip_serializing_if = "is_false")]
1622 pub direct_sink: bool,
1623}
1624
1625fn is_false(b: &bool) -> bool {
1628 !*b
1629}
1630
1631#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1633pub struct VulnSummary {
1634 pub total_findings: u32,
1636 #[serde(default)]
1638 pub by_severity: HashMap<String, u32>,
1639 #[serde(default)]
1641 pub by_type: HashMap<String, u32>,
1642 pub files_with_vulns: u32,
1644}
1645
1646#[derive(Debug, Clone, Serialize, Deserialize)]
1648pub struct VulnReport {
1649 pub findings: Vec<VulnFinding>,
1651 #[serde(skip_serializing_if = "Option::is_none")]
1653 pub summary: Option<VulnSummary>,
1654 pub scan_duration_ms: u64,
1656 pub files_scanned: u32,
1658 #[serde(default, skip_serializing_if = "is_zero_u32")]
1667 pub files_skipped: u32,
1668 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1673 pub warnings: Vec<String>,
1674}
1675
1676impl VulnReport {
1677 pub fn new() -> Self {
1679 Self {
1680 findings: Vec::new(),
1681 summary: None,
1682 scan_duration_ms: 0,
1683 files_scanned: 0,
1684 files_skipped: 0,
1685 warnings: Vec::new(),
1686 }
1687 }
1688}
1689
1690impl Default for VulnReport {
1691 fn default() -> Self {
1692 Self::new()
1693 }
1694}
1695
1696#[cfg(test)]
1701mod unit_types_tests {
1702 use super::*;
1703
1704 #[test]
1709 fn test_output_format_serialization() {
1710 let json = serde_json::to_string(&OutputFormat::Json).unwrap();
1711 assert_eq!(json, r#""json""#);
1712
1713 let text = serde_json::to_string(&OutputFormat::Text).unwrap();
1714 assert_eq!(text, r#""text""#);
1715
1716 let sarif = serde_json::to_string(&OutputFormat::Sarif).unwrap();
1717 assert_eq!(sarif, r#""sarif""#);
1718 }
1719
1720 #[test]
1721 fn test_output_format_deserialization() {
1722 let json: OutputFormat = serde_json::from_str(r#""json""#).unwrap();
1723 assert_eq!(json, OutputFormat::Json);
1724
1725 let text: OutputFormat = serde_json::from_str(r#""text""#).unwrap();
1726 assert_eq!(text, OutputFormat::Text);
1727 }
1728
1729 #[test]
1734 fn test_severity_ordering() {
1735 assert!(Severity::Critical.order() < Severity::High.order());
1736 assert!(Severity::High.order() < Severity::Medium.order());
1737 assert!(Severity::Medium.order() < Severity::Low.order());
1738 assert!(Severity::Low.order() < Severity::Info.order());
1739 }
1740
1741 #[test]
1742 fn test_severity_serialization() {
1743 let critical = serde_json::to_string(&Severity::Critical).unwrap();
1744 assert_eq!(critical, r#""critical""#);
1745
1746 let info = serde_json::to_string(&Severity::Info).unwrap();
1747 assert_eq!(info, r#""info""#);
1748 }
1749
1750 #[test]
1755 fn test_location_serialization() {
1756 let loc = Location::new("test.py", 42);
1757 let json = serde_json::to_string(&loc).unwrap();
1758 assert!(json.contains(r#""file":"test.py""#));
1759 assert!(json.contains(r#""line":42"#));
1760 }
1761
1762 #[test]
1763 fn test_location_with_column() {
1764 let loc = Location::with_column("test.py", 42, 10);
1765 assert_eq!(loc.column, 10);
1766 }
1767
1768 #[test]
1773 fn test_todo_report_serialization() {
1774 let mut report = TodoReport::new("/path/to/file.py");
1775 report
1776 .items
1777 .push(TodoItem::new("dead_code", 1, "Unused function"));
1778 report.summary.dead_count = 1;
1779
1780 let json = serde_json::to_string(&report).unwrap();
1781 assert!(json.contains(r#""wrapper":"todo""#));
1782 assert!(json.contains(r#""dead_count":1"#));
1783 }
1784
1785 #[test]
1786 fn test_todo_item_builder() {
1787 let item = TodoItem::new("complexity", 2, "High cyclomatic complexity")
1788 .with_location("src/main.py", 100)
1789 .with_severity("high")
1790 .with_score(0.85);
1791
1792 assert_eq!(item.category, "complexity");
1793 assert_eq!(item.file, "src/main.py");
1794 assert_eq!(item.line, 100);
1795 assert_eq!(item.severity, "high");
1796 assert!((item.score - 0.85).abs() < 0.001);
1797 }
1798
1799 #[test]
1804 fn test_explain_report_serialization() {
1805 let mut report = ExplainReport::new("calculate_total", "/path/file.py", 10, 20, "python");
1806 report.purity = PurityInfo::pure();
1807
1808 let json = serde_json::to_string(&report).unwrap();
1809 assert!(json.contains(r#""function":"calculate_total""#));
1811 assert!(json.contains(r#""classification":"pure""#));
1812 }
1813
1814 #[test]
1815 fn test_signature_info_builder() {
1816 let sig = SignatureInfo::new()
1817 .with_param(ParamInfo::new("x").with_type("int"))
1818 .with_return_type("int")
1819 .with_docstring("Doubles the input");
1820
1821 assert_eq!(sig.params.len(), 1);
1822 assert_eq!(sig.params[0].name, "x");
1823 assert_eq!(sig.return_type.unwrap(), "int");
1824 }
1825
1826 #[test]
1831 fn test_secure_report_serialization() {
1832 let mut report = SecureReport::new("/path/to/file.py");
1833 report
1834 .findings
1835 .push(SecureFinding::new("taint", "critical", "SQL injection"));
1836
1837 let json = serde_json::to_string(&report).unwrap();
1838 assert!(json.contains(r#""wrapper":"secure""#));
1839 }
1840
1841 #[test]
1846 fn test_definition_result_serialization() {
1847 let result = DefinitionResult {
1848 symbol: SymbolInfo::new("my_func", SymbolKind::Function),
1849 definition: Some(Location::new("file.py", 10)),
1850 type_definition: None,
1851 };
1852
1853 let json = serde_json::to_string(&result).unwrap();
1854 assert!(json.contains(r#""name":"my_func""#));
1855 assert!(json.contains(r#""kind":"function""#));
1856 }
1857
1858 #[test]
1859 fn test_symbol_kind_serialization() {
1860 let kind = SymbolKind::Function;
1861 let json = serde_json::to_string(&kind).unwrap();
1862 assert_eq!(json, r#""function""#);
1863 }
1864
1865 #[test]
1870 fn test_diff_report_serialization() {
1871 let mut report = DiffReport::new("a.py", "b.py");
1872 report.identical = false;
1873 if let Some(ref mut summary) = report.summary {
1874 summary.inserts = 1;
1875 }
1876
1877 let json = serde_json::to_string(&report).unwrap();
1878 assert!(json.contains(r#""file_a":"a.py""#));
1879 assert!(json.contains(r#""identical":false"#));
1880 }
1881
1882 #[test]
1883 fn test_change_type_serialization() {
1884 let insert = serde_json::to_string(&ChangeType::Insert).unwrap();
1885 assert_eq!(insert, r#""insert""#);
1886
1887 let rename = serde_json::to_string(&ChangeType::Rename).unwrap();
1888 assert_eq!(rename, r#""rename""#);
1889 }
1890
1891 #[test]
1896 fn test_api_check_report_serialization() {
1897 let mut report = APICheckReport::new();
1898 report.rules_applied = 5;
1899 report.summary.total_findings = 2;
1900
1901 let json = serde_json::to_string(&report).unwrap();
1902 assert!(json.contains(r#""rules_applied":5"#));
1903 assert!(json.contains(r#""total_findings":2"#));
1904 }
1905
1906 #[test]
1911 fn test_gvn_report_serialization() {
1912 let mut report = GVNReport::new("test_func");
1913 report.summary.total_expressions = 10;
1914 report.summary.unique_values = 7;
1915 report.summary.compression_ratio = 0.7;
1916
1917 let json = serde_json::to_string(&report).unwrap();
1918 assert!(json.contains(r#""function":"test_func""#));
1919 assert!(json.contains(r#""compression_ratio":0.7"#));
1920 }
1921
1922 #[test]
1927 fn test_vuln_report_serialization() {
1928 let mut report = VulnReport::new();
1929 report.files_scanned = 5;
1930 report.scan_duration_ms = 100;
1931
1932 let json = serde_json::to_string(&report).unwrap();
1933 assert!(json.contains(r#""files_scanned":5"#));
1934 assert!(json.contains(r#""scan_duration_ms":100"#));
1935 }
1936
1937 #[test]
1938 fn test_vuln_type_cwe_mapping() {
1939 assert_eq!(VulnType::SqlInjection.cwe_id(), "CWE-89");
1940 assert_eq!(VulnType::Xss.cwe_id(), "CWE-79");
1941 assert_eq!(VulnType::CommandInjection.cwe_id(), "CWE-78");
1942 assert_eq!(VulnType::Ssrf.cwe_id(), "CWE-918");
1943 assert_eq!(VulnType::PathTraversal.cwe_id(), "CWE-22");
1944 assert_eq!(VulnType::Deserialization.cwe_id(), "CWE-502");
1945 assert_eq!(VulnType::UnsafeCode.cwe_id(), "CWE-242");
1946 assert_eq!(VulnType::MemorySafety.cwe_id(), "CWE-119");
1947 assert_eq!(VulnType::Panic.cwe_id(), "CWE-703");
1948 assert_eq!(VulnType::Xxe.cwe_id(), "CWE-611");
1949 assert_eq!(VulnType::OpenRedirect.cwe_id(), "CWE-601");
1950 assert_eq!(VulnType::LdapInjection.cwe_id(), "CWE-90");
1951 assert_eq!(VulnType::XpathInjection.cwe_id(), "CWE-643");
1952 }
1953
1954 #[test]
1955 fn test_vuln_type_default_severity() {
1956 assert_eq!(
1957 VulnType::SqlInjection.default_severity(),
1958 Severity::Critical
1959 );
1960 assert_eq!(
1961 VulnType::CommandInjection.default_severity(),
1962 Severity::Critical
1963 );
1964 assert_eq!(
1965 VulnType::MemorySafety.default_severity(),
1966 Severity::Critical
1967 );
1968 assert_eq!(VulnType::Xss.default_severity(), Severity::High);
1969 assert_eq!(VulnType::UnsafeCode.default_severity(), Severity::High);
1970 assert_eq!(VulnType::OpenRedirect.default_severity(), Severity::Medium);
1971 assert_eq!(VulnType::Panic.default_severity(), Severity::Medium);
1972 }
1973
1974 #[test]
1975 fn test_vuln_type_serialization() {
1976 let sql_inj = serde_json::to_string(&VulnType::SqlInjection).unwrap();
1977 assert_eq!(sql_inj, r#""sql_injection""#);
1978
1979 let xss = serde_json::to_string(&VulnType::Xss).unwrap();
1980 assert_eq!(xss, r#""xss""#);
1981 }
1982}