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