Skip to main content

ai_agent/review/
mod.rs

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}