1use crate::{
2 context::LintContext,
3 lint::{RuleViolation, Severity},
4};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum RuleCategory {
8 Naming,
11 Formatting,
13 Idioms,
15 ErrorHandling,
17 CodeQuality,
19 Documentation,
21 TypeSafety,
23 Performance,
25}
26
27impl RuleCategory {
28 #[must_use]
30 pub const fn as_str(self) -> &'static str {
31 match self {
32 RuleCategory::Naming => "naming",
33 RuleCategory::Formatting => "formatting",
34 RuleCategory::Idioms => "idioms",
35 RuleCategory::ErrorHandling => "error-handling",
36 RuleCategory::CodeQuality => "code-quality",
37 RuleCategory::Documentation => "documentation",
38 RuleCategory::TypeSafety => "type-safety",
39 RuleCategory::Performance => "performance",
40 }
41 }
42}
43
44impl std::fmt::Display for RuleCategory {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 f.write_str(self.as_str())
47 }
48}
49
50pub struct Rule {
52 pub id: &'static str,
53 pub category: RuleCategory,
54 pub severity: Severity,
55 pub description: &'static str,
56 pub check: fn(&LintContext) -> Vec<RuleViolation>,
57}
58
59impl Rule {
60 pub const fn new(
62 id: &'static str,
63 category: RuleCategory,
64 severity: Severity,
65 description: &'static str,
66 check: fn(&LintContext) -> Vec<RuleViolation>,
67 ) -> Self {
68 Self {
69 id,
70 category,
71 severity,
72 description,
73 check,
74 }
75 }
76
77 #[must_use]
79 pub fn check(&self, context: &LintContext) -> Vec<RuleViolation> {
80 (self.check)(context)
81 }
82
83 #[cfg(test)]
84 #[allow(clippy::missing_panics_doc)]
85 #[track_caller]
86 pub fn assert_detects(&self, code: &str) {
88 let violations = LintContext::test_with_parsed_source(code, |context| self.check(&context));
89 assert!(
90 !violations.is_empty(),
91 "Expected rule '{}' to detect violations in code, but found none",
92 self.id
93 );
94 }
95
96 #[cfg(test)]
97 #[allow(clippy::missing_panics_doc)]
98 #[track_caller]
99 pub fn assert_ignores(&self, code: &str) {
101 let violations = LintContext::test_with_parsed_source(code, |context| self.check(&context));
102 assert!(
103 violations.is_empty(),
104 "Expected rule '{}' to ignore code, but found {} violations",
105 self.id,
106 violations.len()
107 );
108 }
109
110 #[cfg(test)]
111 #[allow(clippy::missing_panics_doc)]
112 #[track_caller]
113 pub fn assert_violation_count(&self, code: &str, expected_min: usize) {
116 let violations = LintContext::test_with_parsed_source(code, |context| self.check(&context));
117 assert!(
118 violations.len() >= expected_min,
119 "Expected rule '{}' to find at least {} violations, but found {}",
120 self.id,
121 expected_min,
122 violations.len()
123 );
124 }
125
126 #[cfg(test)]
127 #[allow(clippy::missing_panics_doc)]
128 #[track_caller]
129 pub fn assert_violation_count_exact(&self, code: &str, expected: usize) {
132 let violations = LintContext::test_with_parsed_source(code, |context| self.check(&context));
133 assert_eq!(
134 violations.len(),
135 expected,
136 "Expected rule '{}' to find exactly {} violation(s), but found {}",
137 self.id,
138 expected,
139 violations.len()
140 );
141 }
142}