1#[derive(Debug, Clone, PartialEq)]
2pub enum FindingSeverity {
3 Info,
4 Warning,
5 Error,
6 Critical,
7}
8
9#[derive(Debug, Clone)]
10pub struct Finding {
11 pub severity: FindingSeverity,
12 pub message: String,
13 pub file: Option<String>,
14 pub line: Option<u32>,
15 pub rule: Option<String>,
16}
17
18impl Finding {
19 pub fn info(message: &str) -> Self {
20 Self {
21 severity: FindingSeverity::Info,
22 message: message.to_string(),
23 file: None,
24 line: None,
25 rule: None,
26 }
27 }
28
29 pub fn warning(message: &str) -> Self {
30 Self {
31 severity: FindingSeverity::Warning,
32 message: message.to_string(),
33 file: None,
34 line: None,
35 rule: None,
36 }
37 }
38
39 pub fn error(message: &str) -> Self {
40 Self {
41 severity: FindingSeverity::Error,
42 message: message.to_string(),
43 file: None,
44 line: None,
45 rule: None,
46 }
47 }
48
49 pub fn with_file(mut self, file: &str) -> Self {
50 self.file = Some(file.to_string());
51 self
52 }
53
54 pub fn with_line(mut self, line: u32) -> Self {
55 self.line = Some(line);
56 self
57 }
58
59 pub fn with_rule(mut self, rule: &str) -> Self {
60 self.rule = Some(rule.to_string());
61 self
62 }
63}
64
65#[derive(Debug, Clone)]
66pub struct ReviewResult {
67 pub findings: Vec<Finding>,
68 pub summary: String,
69 pub score: Option<u32>,
70}
71
72impl ReviewResult {
73 pub fn new(findings: Vec<Finding>, summary: &str) -> Self {
74 let score = Self::calculate_score(&findings);
75 Self {
76 findings,
77 summary: summary.to_string(),
78 score: Some(score),
79 }
80 }
81
82 fn calculate_score(findings: &[Finding]) -> u32 {
83 let mut score = 100u32;
84 for finding in findings {
85 match finding.severity {
86 FindingSeverity::Info => score -= 1,
87 FindingSeverity::Warning => score -= 5,
88 FindingSeverity::Error => score -= 10,
89 FindingSeverity::Critical => score -= 25,
90 }
91 }
92 score.saturating_sub(0)
93 }
94
95 pub fn error_count(&self) -> usize {
96 self.findings
97 .iter()
98 .filter(|f| {
99 matches!(
100 f.severity,
101 FindingSeverity::Error | FindingSeverity::Critical
102 )
103 })
104 .count()
105 }
106
107 pub fn warning_count(&self) -> usize {
108 self.findings
109 .iter()
110 .filter(|f| matches!(f.severity, FindingSeverity::Warning))
111 .count()
112 }
113}
114
115pub fn review_code(code: &str, language: &str) -> ReviewResult {
116 let mut findings = Vec::new();
117
118 match language {
119 "rust" => {
120 if code.contains("unsafe") {
121 findings.push(
122 Finding::warning("Use of unsafe code detected").with_rule("security/unsafe"),
123 );
124 }
125 if code.contains("expect(") || code.contains("unwrap(") {
126 findings.push(
127 Finding::warning("Potential panic with expect/unwrap")
128 .with_rule("style/expect"),
129 );
130 }
131 }
132 "typescript" | "javascript" => {
133 if code.contains("eval(") {
134 findings
135 .push(Finding::error("Use of eval() is dangerous").with_rule("security/eval"));
136 }
137 if code.contains("console.log") && code.contains("TODO") {
138 findings.push(Finding::info("Debug logging left in code").with_rule("style/debug"));
139 }
140 }
141 _ => {}
142 }
143
144 if code.len() > 10000 {
145 findings.push(
146 Finding::warning("File is very large, consider splitting")
147 .with_rule("maintainability size"),
148 );
149 }
150
151 let summary = format!(
152 "Found {} issues: {} errors, {} warnings",
153 findings.len(),
154 findings
155 .iter()
156 .filter(|f| matches!(
157 f.severity,
158 FindingSeverity::Error | FindingSeverity::Critical
159 ))
160 .count(),
161 findings
162 .iter()
163 .filter(|f| matches!(f.severity, FindingSeverity::Warning))
164 .count()
165 );
166
167 ReviewResult::new(findings, &summary)
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_finding_creation() {
176 let finding = Finding::error("Test error")
177 .with_file("src/main.rs")
178 .with_line(42)
179 .with_rule("test/rule");
180
181 assert_eq!(finding.severity, FindingSeverity::Error);
182 assert_eq!(finding.file, Some("src/main.rs".to_string()));
183 assert_eq!(finding.line, Some(42));
184 }
185
186 #[test]
187 fn test_review_result_score() {
188 let findings = vec![
189 Finding::error("Error 1"),
190 Finding::warning("Warning 1"),
191 Finding::info("Info 1"),
192 ];
193 let result = ReviewResult::new(findings, "Test review");
194 assert_eq!(result.score, Some(84));
195 }
196}