1#[cfg(test)]
2use std::borrow::Cow;
3use std::{
4 any::TypeId,
5 fmt::{Debug, Formatter, Result as FmtResult},
6 hash::{Hash, Hasher},
7};
8
9use lsp_types::DiagnosticTag;
10
11use crate::{
12 Fix, LintLevel,
13 context::LintContext,
14 violation::{Detection, Violation},
15};
16
17pub trait DetectFix: Send + Sync + 'static {
19 type FixInput<'a>: Send + Sync;
21
22 fn id(&self) -> &'static str;
25
26 fn level(&self) -> LintLevel;
28
29 fn detect<'a>(&self, context: &'a LintContext) -> Vec<(Detection, Self::FixInput<'a>)>;
31
32 fn short_description(&self) -> &'static str;
34
35 fn long_description(&self) -> Option<&'static str> {
38 None
39 }
40
41 fn source_link(&self) -> Option<&'static str> {
44 None
45 }
46
47 fn conflicts_with(&self) -> &'static [&'static dyn Rule] {
50 &[]
51 }
52
53 fn fix(&self, _context: &LintContext, _fix_data: &Self::FixInput<'_>) -> Option<Fix> {
54 None
55 }
56
57 fn diagnostic_tags(&self) -> &'static [DiagnosticTag] {
60 &[]
61 }
62
63 fn no_fix<'a>(detections: Vec<Detection>) -> Vec<(Detection, Self::FixInput<'a>)>
66 where
67 Self::FixInput<'a>: Default,
68 {
69 detections
70 .into_iter()
71 .map(|v| (v, Self::FixInput::default()))
72 .collect()
73 }
74}
75
76pub trait Rule: Send + Sync {
81 fn id(&self) -> &'static str;
82 fn short_description(&self) -> &'static str;
83 fn source_link(&self) -> Option<&'static str>;
84 fn level(&self) -> LintLevel;
85 fn has_auto_fix(&self) -> bool;
86 fn conflicts_with(&self) -> &'static [&'static dyn Rule];
87 fn diagnostic_tags(&self) -> &'static [DiagnosticTag];
88 fn check(&self, context: &LintContext) -> Vec<Violation>;
89}
90
91impl<T: DetectFix> Rule for T {
92 fn id(&self) -> &'static str {
93 DetectFix::id(self)
94 }
95
96 fn short_description(&self) -> &'static str {
97 DetectFix::short_description(self)
98 }
99
100 fn source_link(&self) -> Option<&'static str> {
101 DetectFix::source_link(self)
102 }
103
104 fn level(&self) -> LintLevel {
105 DetectFix::level(self)
106 }
107
108 fn has_auto_fix(&self) -> bool {
109 TypeId::of::<T::FixInput<'static>>() != TypeId::of::<()>()
110 }
111
112 fn conflicts_with(&self) -> &'static [&'static dyn Rule] {
113 DetectFix::conflicts_with(self)
114 }
115
116 fn diagnostic_tags(&self) -> &'static [DiagnosticTag] {
117 DetectFix::diagnostic_tags(self)
118 }
119
120 fn check(&self, context: &LintContext) -> Vec<Violation> {
121 self.detect(context)
122 .into_iter()
123 .map(|(detected, fix_data)| {
124 let long_description = self.long_description();
125 let fix = self.fix(context, &fix_data);
126 Violation::from_detected(detected, fix, long_description)
127 })
128 .collect()
129 }
130}
131
132impl Debug for dyn Rule {
133 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
134 f.debug_struct("Rule")
135 .field("id", &self.id())
136 .field("level", &self.level())
137 .field("has_auto_fix", &self.has_auto_fix())
138 .finish()
139 }
140}
141
142impl Hash for dyn Rule {
143 fn hash<H: Hasher>(&self, state: &mut H) {
144 self.id().hash(state);
145 }
146}
147
148impl PartialEq for dyn Rule {
149 fn eq(&self, other: &Self) -> bool {
150 self.id() == other.id()
151 }
152}
153
154impl Eq for dyn Rule {}
155
156#[cfg(test)]
157impl dyn Rule {
158 fn run_check(&self, code: &str) -> Vec<Violation> {
159 LintContext::test_get_violations(code, |context| self.check(context))
160 }
161
162 #[track_caller]
163 fn first_violation(&self, code: &str) -> Violation {
164 let violations = self.run_check(code);
165 assert!(
166 !violations.is_empty(),
167 "Expected rule '{}' to detect violations, but found none",
168 self.id()
169 );
170 violations.into_iter().next().unwrap()
171 }
172
173 pub fn first_replacement_text(&self, code: &str) -> Cow<'static, str> {
174 let fix = self
175 .first_violation(code)
176 .fix
177 .expect("Expected violation to have a fix");
178 assert!(
179 !fix.replacements.is_empty(),
180 "Expected fix to have replacements"
181 );
182 fix.replacements
183 .into_iter()
184 .next()
185 .unwrap()
186 .replacement_text
187 }
188
189 pub fn apply_first_fix(&self, code: &str) -> String {
192 use std::cmp::Reverse;
193
194 let violation = self.first_violation(code);
195 let fix = violation.fix.expect("Expected violation to have a fix");
196 assert!(
197 !fix.replacements.is_empty(),
198 "Expected fix to have replacements"
199 );
200
201 let mut replacements = fix.replacements;
202 replacements.sort_by_key(|b| Reverse(b.file_span().start));
203
204 let mut result = code.to_string();
205 for replacement in replacements {
206 let start = replacement.file_span().start;
207 let end = replacement.file_span().end;
208 result.replace_range(start..end, &replacement.replacement_text);
209 }
210 result
211 }
212
213 #[track_caller]
214 pub fn assert_detects(&self, code: &str) {
215 let violations = self.run_check(code);
216 assert!(
217 !violations.is_empty(),
218 "Expected rule '{}' to detect violations in code, but found none",
219 self.id()
220 );
221 }
222
223 #[track_caller]
224 pub fn assert_ignores(&self, code: &str) {
225 let violations = self.run_check(code);
226 assert!(
227 violations.is_empty(),
228 "Expected rule '{}' to ignore code, but found {} violations",
229 self.id(),
230 violations.len()
231 );
232 }
233
234 #[track_caller]
235 pub fn assert_count(&self, code: &str, expected: usize) {
236 let violations = self.run_check(code);
237 assert_eq!(
238 violations.len(),
239 expected,
240 "Expected rule '{}' to find exactly {} violation(s), but found {}",
241 self.id(),
242 expected,
243 violations.len()
244 );
245 }
246
247 #[track_caller]
248 pub fn assert_fixed_contains(&self, code: &str, expected_text: &str) {
249 let fixed = self.apply_first_fix(code);
250 assert!(
251 fixed.contains(expected_text),
252 "Expected fixed code to contain `{expected_text}`, but it didn't, it was `{fixed}`"
253 );
254 }
255
256 #[track_caller]
257 pub fn assert_fixed_not_contains(&self, code: &str, unexpected_text: &str) {
258 let fixed = self.apply_first_fix(code);
259 assert!(
260 !fixed.contains(unexpected_text),
261 "Expected fixed code NOT to contain `{unexpected_text}`, but it did: `{fixed}`"
262 );
263 }
264
265 #[track_caller]
266 pub fn assert_fixed_is(&self, bad_code: &str, expected_code: &str) {
267 let fixed = self.apply_first_fix(bad_code);
268 assert!(
269 fixed == expected_code,
270 "Expected fix to be `{fixed}` but received `{expected_code}`"
271 );
272 }
273
274 #[track_caller]
275 pub fn assert_labels_contain(&self, code: &str, expected_text: &str) {
276 let violation = self.first_violation(code);
277 let label_texts: Vec<&str> = violation
278 .extra_labels
279 .iter()
280 .filter_map(|(_, label)| label.as_deref())
281 .collect();
282
283 assert!(
284 label_texts.iter().any(|t| t.contains(expected_text)),
285 "Expected a label to contain '{expected_text}', but got labels: {label_texts:?}"
286 );
287 }
288
289 #[track_caller]
290 pub fn assert_fix_erases(&self, code: &str, erased_text: &str) {
291 let fixed = self.apply_first_fix(code);
292 assert!(
293 code.contains(erased_text),
294 "Original code should contain '{erased_text}', but it doesn't"
295 );
296 assert!(
297 !fixed.contains(erased_text),
298 "Expected fixed code to not contain '{erased_text}', but it still appears in: {fixed}"
299 );
300 }
301}