oparry_validators/
rust.rs1use crate::Validator;
4use oparry_core::{Issue, IssueLevel, Result, ValidationResult};
5use oparry_parser::{ParsedCode, Language};
6use regex::Regex;
7use std::path::Path;
8
9#[derive(Debug, Clone)]
11pub struct RustConfig {
12 pub deny_unsafe: bool,
14 pub warn_unwrap: bool,
16 pub enforce_result_handling: bool,
18}
19
20impl Default for RustConfig {
21 fn default() -> Self {
22 Self {
23 deny_unsafe: false,
24 warn_unwrap: true,
25 enforce_result_handling: true,
26 }
27 }
28}
29
30pub struct RustValidator {
32 config: RustConfig,
33 unwrap_regex: Regex,
34 expect_regex: Regex,
35 unsafe_regex: Regex,
36}
37
38impl RustValidator {
39 pub fn new(config: RustConfig) -> Self {
41 Self {
42 config,
43 unwrap_regex: Regex::new(r#"\.unwrap\(\)"#).unwrap(),
44 expect_regex: Regex::new(r#"\.expect\("[^"]*"\)"#).unwrap(),
45 unsafe_regex: Regex::new(r"\bunsafe\b").unwrap(),
46 }
47 }
48
49 pub fn default_config() -> Self {
51 Self::new(RustConfig::default())
52 }
53
54 fn check_unwrap(&self, line: &str, file: &str, line_idx: usize) -> Option<Issue> {
56 if self.config.warn_unwrap {
57 if self.unwrap_regex.is_match(line) {
58 return Some(Issue::warning(
59 "rust-unwrap",
60 "Use of .unwrap() may cause panic",
61 )
62 .with_file(file)
63 .with_line(line_idx)
64 .with_suggestion("Use proper error handling with ? or match"));
65 }
66 }
67 None
68 }
69
70 fn check_unsafe(&self, line: &str, file: &str, line_idx: usize) -> Option<Issue> {
72 if self.config.deny_unsafe && self.unsafe_regex.is_match(line) {
73 return Some(Issue::error(
74 "rust-unsafe",
75 "Unsafe code is not allowed",
76 )
77 .with_file(file)
78 .with_line(line_idx)
79 .with_suggestion("Remove unsafe block or update configuration"));
80 }
81 None
82 }
83}
84
85impl Validator for RustValidator {
86 fn name(&self) -> &str {
87 "Rust"
88 }
89
90 fn supports(&self, language: Language) -> bool {
91 language.is_rust()
92 }
93
94 fn validate_parsed(&self, code: &ParsedCode, file: &Path) -> Result<ValidationResult> {
95 let mut result = ValidationResult::new();
96 let source = code.source();
97
98 let file_str = file.to_string_lossy().to_string();
99
100 for (line_idx, line) in source.lines().enumerate() {
101 if let Some(issue) = self.check_unwrap(line, &file_str, line_idx) {
103 result.add_issue(issue);
104 }
105
106 if let Some(issue) = self.check_unsafe(line, &file_str, line_idx) {
108 result.add_issue(issue);
109 }
110 }
111
112 Ok(result)
113 }
114
115 fn validate_raw(&self, source: &str, file: &Path) -> Result<ValidationResult> {
116 let parsed = ParsedCode::Generic(source.to_string());
117 self.validate_parsed(&parsed, file)
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_rust_validator_unwrap() {
127 let validator = RustValidator::default_config();
128 let code = r#"
129 fn main() {
130 let x = Some(5);
131 let y = x.unwrap();
132 }
133 "#;
134
135 let result = validator.validate_raw(code, Path::new("test.rs")).unwrap();
136 assert!(result.warning_count() > 0, "Should detect unwrap usage");
138 assert_eq!(result.issues[0].code, "rust-unwrap");
139 }
140
141 #[test]
142 fn test_rust_validator_unsafe() {
143 let config = RustConfig {
144 deny_unsafe: true,
145 ..Default::default()
146 };
147 let validator = RustValidator::new(config);
148 let code = r#"
149 fn main() {
150 unsafe {
151 println!("Hello");
152 }
153 }
154 "#;
155
156 let result = validator.validate_raw(code, Path::new("test.rs")).unwrap();
157 assert!(!result.passed);
158 assert_eq!(result.issues[0].code, "rust-unsafe");
159 }
160
161 #[test]
162 fn test_rust_validator_safe() {
163 let validator = RustValidator::default_config();
164 let code = r#"
165 fn main() {
166 let x = Some(5);
167 let y = x.unwrap_or(0);
168 println!("{}", y);
169 }
170 "#;
171
172 let result = validator.validate_raw(code, Path::new("test.rs")).unwrap();
173 assert!(result.passed);
174 }
175
176 #[test]
177 fn test_rust_config_default() {
178 let config = RustConfig::default();
179 assert!(!config.deny_unsafe);
180 assert!(config.warn_unwrap);
181 assert!(config.enforce_result_handling);
182 }
183
184 #[test]
185 fn test_rust_validator_no_unwrap_warning() {
186 let config = RustConfig {
187 warn_unwrap: false,
188 ..Default::default()
189 };
190 let validator = RustValidator::new(config);
191 let code = r#"
192 fn main() {
193 let x = Some(5);
194 let y = x.unwrap();
195 }
196 "#;
197
198 let result = validator.validate_raw(code, Path::new("test.rs")).unwrap();
199 assert!(result.passed);
200 }
201
202 #[test]
203 fn test_rust_validator_expect() {
204 let validator = RustValidator::default_config();
205 let code = r#"
206 fn main() {
207 let x = Some(5);
208 let y = x.expect("must have value");
209 }
210 "#;
211
212 let result = validator.validate_raw(code, Path::new("test.rs")).unwrap();
214 assert!(result.passed);
216 }
217
218 #[test]
219 fn test_rust_validator_supports() {
220 let validator = RustValidator::default_config();
221 assert!(validator.supports(Language::Rust));
222 assert!(!validator.supports(Language::JavaScript));
223 assert!(!validator.supports(Language::TypeScript));
224 }
225
226 #[test]
227 fn test_rust_validator_multiple_unwraps() {
228 let validator = RustValidator::default_config();
229 let code = r#"
230 fn main() {
231 let x = Some(5);
232 let y = x.unwrap();
233 let z = y.unwrap();
234 }
235 "#;
236
237 let result = validator.validate_raw(code, Path::new("test.rs")).unwrap();
238 assert_eq!(result.issues.len(), 2);
239 }
240
241 #[test]
242 fn test_rust_validator_unsafe_fn_keyword() {
243 let config = RustConfig {
244 deny_unsafe: true,
245 ..Default::default()
246 };
247 let validator = RustValidator::new(config);
248 let code = r#"
249 unsafe fn dangerous() {}
250 "#;
251
252 let result = validator.validate_raw(code, Path::new("test.rs")).unwrap();
253 assert!(!result.passed);
254 assert_eq!(result.issues[0].code, "rust-unsafe");
255 }
256}