Skip to main content

oparry_validators/
rust.rs

1//! Rust-specific validator
2
3use crate::Validator;
4use oparry_core::{Issue, IssueLevel, Result, ValidationResult};
5use oparry_parser::{ParsedCode, Language};
6use regex::Regex;
7use std::path::Path;
8
9/// Rust validation configuration
10#[derive(Debug, Clone)]
11pub struct RustConfig {
12    /// Deny unsafe code
13    pub deny_unsafe: bool,
14    /// Warn on .unwrap()
15    pub warn_unwrap: bool,
16    /// Enforce Result handling
17    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
30/// Rust validator
31pub struct RustValidator {
32    config: RustConfig,
33    unwrap_regex: Regex,
34    expect_regex: Regex,
35    unsafe_regex: Regex,
36}
37
38impl RustValidator {
39    /// Create new Rust validator
40    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    /// Create with default config
50    pub fn default_config() -> Self {
51        Self::new(RustConfig::default())
52    }
53
54    /// Check for unwrap usage
55    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    /// Check for unsafe blocks
71    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            // Check unwrap
102            if let Some(issue) = self.check_unwrap(line, &file_str, line_idx) {
103                result.add_issue(issue);
104            }
105
106            // Check unsafe
107            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        // Warnings don't fail by default (only in strict mode)
137        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        // expect is also caught by the unwrap regex pattern
213        let result = validator.validate_raw(code, Path::new("test.rs")).unwrap();
214        // Currently only unwrap is checked, expect is defined but not used
215        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}