1use crate::output::{LintMessage, Severity};
2use crate::rules::Rule;
3use regex::Regex;
4use serde::Deserialize;
5use std::path::Path;
6
7#[derive(Debug, Clone, Deserialize)]
8pub struct CustomRuleDefinition {
9 pub name: String,
10 pub pattern: String,
11 pub message: String,
12 pub severity: String,
13 pub suggestion: Option<String>,
14 pub extensions: Option<Vec<String>>,
15}
16
17#[derive(Debug, Clone)]
18pub struct CustomRule {
19 definition: CustomRuleDefinition,
20 regex: Regex,
21}
22
23impl CustomRule {
24 pub fn from_definition(def: CustomRuleDefinition) -> Result<Self, regex::Error> {
25 let regex = Regex::new(&def.pattern)?;
26 Ok(Self {
27 definition: def,
28 regex,
29 })
30 }
31}
32
33impl Rule for CustomRule {
34 fn name(&self) -> &str {
35 &self.definition.name
36 }
37
38 fn category(&self) -> &str {
39 "custom"
40 }
41
42 fn check(&self, content: &str, file_path: &Path) -> Vec<LintMessage> {
43 if let Some(ref extensions) = self.definition.extensions {
44 let ext_matches = file_path.extension().is_some_and(|ext| {
45 let ext_str = ext.to_string_lossy();
46 extensions.iter().any(|e| e == ext_str.as_ref())
47 });
48 if !ext_matches {
49 return Vec::new();
50 }
51 }
52
53 let mut messages = Vec::new();
54 let severity = match self.definition.severity.as_str() {
55 "Error" => Severity::Error,
56 "Warning" => Severity::Warning,
57 _ => Severity::Info,
58 };
59
60 for (line_num, line) in content.lines().enumerate() {
61 if self.regex.is_match(line) {
62 messages.push(LintMessage::new(
63 line_num + 1,
64 0,
65 severity,
66 self.definition.message.clone(),
67 self.definition.name.clone(),
68 self.definition.suggestion.clone(),
69 ));
70 }
71 }
72
73 messages
74 }
75}