1use std::cmp::Ordering;
10use std::fmt;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum Severity {
18 Error,
20 Warning,
22 Info,
24 Style,
26}
27
28impl Severity {
29 pub fn from_str(s: &str) -> Option<Self> {
31 match s.to_lowercase().as_str() {
32 "error" | "critical" | "major" => Some(Self::Error),
33 "warning" | "minor" => Some(Self::Warning),
34 "info" => Some(Self::Info),
35 "style" => Some(Self::Style),
36 _ => None,
37 }
38 }
39
40 pub fn as_str(&self) -> &'static str {
42 match self {
43 Self::Error => "error",
44 Self::Warning => "warning",
45 Self::Info => "info",
46 Self::Style => "style",
47 }
48 }
49}
50
51impl fmt::Display for Severity {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 write!(f, "{}", self.as_str())
54 }
55}
56
57impl Default for Severity {
58 fn default() -> Self {
59 Self::Warning
60 }
61}
62
63impl Ord for Severity {
64 fn cmp(&self, other: &Self) -> Ordering {
65 let self_val = match self {
67 Self::Error => 0,
68 Self::Warning => 1,
69 Self::Info => 2,
70 Self::Style => 3,
71 };
72 let other_val = match other {
73 Self::Error => 0,
74 Self::Warning => 1,
75 Self::Info => 2,
76 Self::Style => 3,
77 };
78 other_val.cmp(&self_val)
80 }
81}
82
83impl PartialOrd for Severity {
84 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
85 Some(self.cmp(other))
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91pub enum RuleCategory {
92 Style,
94 Security,
96 BestPractice,
98 Performance,
100}
101
102impl RuleCategory {
103 pub fn as_str(&self) -> &'static str {
105 match self {
106 Self::Style => "style",
107 Self::Security => "security",
108 Self::BestPractice => "best-practice",
109 Self::Performance => "performance",
110 }
111 }
112}
113
114impl fmt::Display for RuleCategory {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 write!(f, "{}", self.as_str())
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Hash)]
122pub struct RuleCode(pub String);
123
124impl RuleCode {
125 pub fn new(code: impl Into<String>) -> Self {
127 Self(code.into())
128 }
129
130 pub fn as_str(&self) -> &str {
132 &self.0
133 }
134
135 pub fn is_dcl_rule(&self) -> bool {
137 self.0.starts_with("DCL")
138 }
139
140 pub fn number(&self) -> Option<u32> {
142 if self.0.starts_with("DCL") {
143 self.0[3..].parse().ok()
144 } else {
145 None
146 }
147 }
148}
149
150impl fmt::Display for RuleCode {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 write!(f, "{}", self.0)
153 }
154}
155
156impl From<&str> for RuleCode {
157 fn from(s: &str) -> Self {
158 Self::new(s)
159 }
160}
161
162impl From<String> for RuleCode {
163 fn from(s: String) -> Self {
164 Self(s)
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct CheckFailure {
171 pub code: RuleCode,
173 pub rule_name: String,
175 pub severity: Severity,
177 pub category: RuleCategory,
179 pub message: String,
181 pub line: u32,
183 pub column: u32,
185 pub end_line: Option<u32>,
187 pub end_column: Option<u32>,
189 pub fixable: bool,
191 pub data: std::collections::HashMap<String, String>,
193}
194
195impl CheckFailure {
196 pub fn new(
198 code: impl Into<RuleCode>,
199 rule_name: impl Into<String>,
200 severity: Severity,
201 category: RuleCategory,
202 message: impl Into<String>,
203 line: u32,
204 column: u32,
205 ) -> Self {
206 Self {
207 code: code.into(),
208 rule_name: rule_name.into(),
209 severity,
210 category,
211 message: message.into(),
212 line,
213 column,
214 end_line: None,
215 end_column: None,
216 fixable: false,
217 data: std::collections::HashMap::new(),
218 }
219 }
220
221 pub fn with_end(mut self, end_line: u32, end_column: u32) -> Self {
223 self.end_line = Some(end_line);
224 self.end_column = Some(end_column);
225 self
226 }
227
228 pub fn with_fixable(mut self, fixable: bool) -> Self {
230 self.fixable = fixable;
231 self
232 }
233
234 pub fn with_data(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
236 self.data.insert(key.into(), value.into());
237 self
238 }
239}
240
241impl Ord for CheckFailure {
242 fn cmp(&self, other: &Self) -> Ordering {
243 match self.line.cmp(&other.line) {
245 Ordering::Equal => self.column.cmp(&other.column),
246 other => other,
247 }
248 }
249}
250
251impl PartialOrd for CheckFailure {
252 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
253 Some(self.cmp(other))
254 }
255}
256
257#[derive(Debug, Clone)]
259pub struct RuleMeta {
260 pub description: String,
262 pub url: String,
264}
265
266impl RuleMeta {
267 pub fn new(description: impl Into<String>, url: impl Into<String>) -> Self {
268 Self {
269 description: description.into(),
270 url: url.into(),
271 }
272 }
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277pub enum ConfigLevel {
278 Off = 0,
280 Warn = 1,
282 Error = 2,
284}
285
286impl ConfigLevel {
287 pub fn from_u8(value: u8) -> Option<Self> {
289 match value {
290 0 => Some(Self::Off),
291 1 => Some(Self::Warn),
292 2 => Some(Self::Error),
293 _ => None,
294 }
295 }
296
297 pub fn to_severity(&self) -> Option<Severity> {
299 match self {
300 Self::Off => None,
301 Self::Warn => Some(Severity::Warning),
302 Self::Error => Some(Severity::Error),
303 }
304 }
305}
306
307impl Default for ConfigLevel {
308 fn default() -> Self {
309 Self::Error
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn test_severity_ordering() {
319 assert!(Severity::Error > Severity::Warning);
320 assert!(Severity::Warning > Severity::Info);
321 assert!(Severity::Info > Severity::Style);
322 }
323
324 #[test]
325 fn test_severity_from_str() {
326 assert_eq!(Severity::from_str("error"), Some(Severity::Error));
327 assert_eq!(Severity::from_str("WARNING"), Some(Severity::Warning));
328 assert_eq!(Severity::from_str("Info"), Some(Severity::Info));
329 assert_eq!(Severity::from_str("style"), Some(Severity::Style));
330 assert_eq!(Severity::from_str("critical"), Some(Severity::Error));
331 assert_eq!(Severity::from_str("major"), Some(Severity::Error));
332 assert_eq!(Severity::from_str("minor"), Some(Severity::Warning));
333 assert_eq!(Severity::from_str("invalid"), None);
334 }
335
336 #[test]
337 fn test_rule_code() {
338 let code = RuleCode::new("DCL001");
339 assert!(code.is_dcl_rule());
340 assert_eq!(code.number(), Some(1));
341 assert_eq!(code.as_str(), "DCL001");
342
343 let invalid = RuleCode::new("OTHER");
344 assert!(!invalid.is_dcl_rule());
345 assert_eq!(invalid.number(), None);
346 }
347
348 #[test]
349 fn test_check_failure_ordering() {
350 let f1 = CheckFailure::new(
351 "DCL001",
352 "test",
353 Severity::Warning,
354 RuleCategory::Style,
355 "msg1",
356 5,
357 1,
358 );
359 let f2 = CheckFailure::new(
360 "DCL002",
361 "test",
362 Severity::Info,
363 RuleCategory::Style,
364 "msg2",
365 10,
366 1,
367 );
368 let f3 = CheckFailure::new(
369 "DCL003",
370 "test",
371 Severity::Error,
372 RuleCategory::Style,
373 "msg3",
374 3,
375 1,
376 );
377 let f4 = CheckFailure::new(
378 "DCL004",
379 "test",
380 Severity::Error,
381 RuleCategory::Style,
382 "msg4",
383 3,
384 5,
385 );
386
387 let mut failures = vec![f1.clone(), f2.clone(), f3.clone(), f4.clone()];
388 failures.sort();
389
390 assert_eq!(failures[0].line, 3);
391 assert_eq!(failures[0].column, 1);
392 assert_eq!(failures[1].line, 3);
393 assert_eq!(failures[1].column, 5);
394 assert_eq!(failures[2].line, 5);
395 assert_eq!(failures[3].line, 10);
396 }
397
398 #[test]
399 fn test_config_level() {
400 assert_eq!(ConfigLevel::from_u8(0), Some(ConfigLevel::Off));
401 assert_eq!(ConfigLevel::from_u8(1), Some(ConfigLevel::Warn));
402 assert_eq!(ConfigLevel::from_u8(2), Some(ConfigLevel::Error));
403 assert_eq!(ConfigLevel::from_u8(3), None);
404
405 assert_eq!(ConfigLevel::Off.to_severity(), None);
406 assert_eq!(ConfigLevel::Warn.to_severity(), Some(Severity::Warning));
407 assert_eq!(ConfigLevel::Error.to_severity(), Some(Severity::Error));
408 }
409
410 #[test]
411 fn test_rule_category() {
412 assert_eq!(RuleCategory::Style.as_str(), "style");
413 assert_eq!(RuleCategory::Security.as_str(), "security");
414 assert_eq!(RuleCategory::BestPractice.as_str(), "best-practice");
415 assert_eq!(RuleCategory::Performance.as_str(), "performance");
416 }
417}