1use serde::Deserialize;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum Severity {
17 Info,
19 #[default]
21 Warning,
22 Error,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum RuleKind {
31 Search,
33 Lint,
35 Codemod,
37}
38
39#[derive(Debug, Clone, Default, Deserialize)]
43#[serde(deny_unknown_fields)]
44pub struct Matcher {
45 pub pattern: Option<String>,
48 pub kind: Option<String>,
50 pub regex: Option<String>,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum AtomicMatcher {
57 Pattern(String),
59 Kind(String),
61 Regex(String),
63}
64
65impl Matcher {
66 pub fn resolve(&self) -> Result<AtomicMatcher, String> {
69 let set: Vec<&str> = [
70 self.pattern.as_ref().map(|_| "pattern"),
71 self.kind.as_ref().map(|_| "kind"),
72 self.regex.as_ref().map(|_| "regex"),
73 ]
74 .into_iter()
75 .flatten()
76 .collect();
77 match set.as_slice() {
78 [] => Err("rule block sets none of `pattern` / `kind` / `regex`".into()),
79 [one] => Ok(match *one {
80 "pattern" => AtomicMatcher::Pattern(self.pattern.clone().unwrap()),
81 "kind" => AtomicMatcher::Kind(self.kind.clone().unwrap()),
82 _ => AtomicMatcher::Regex(self.regex.clone().unwrap()),
83 }),
84 many => Err(format!(
85 "rule block sets multiple matchers ({}); set exactly one",
86 many.join(", ")
87 )),
88 }
89 }
90}
91
92#[derive(Debug, Clone, Deserialize)]
94#[serde(deny_unknown_fields)]
95pub struct Rule {
96 pub id: String,
98 pub language: String,
100 #[serde(default)]
102 pub severity: Severity,
103 #[serde(default)]
105 pub message: String,
106 pub rule: Matcher,
108 #[serde(default)]
110 pub fix: Option<String>,
111}
112
113impl Rule {
114 pub fn kind(&self) -> RuleKind {
116 if self.fix.is_some() {
117 RuleKind::Codemod
118 } else if self.message.is_empty() {
119 RuleKind::Search
120 } else {
121 RuleKind::Lint
122 }
123 }
124
125 pub fn from_toml_str(text: &str) -> Result<Self, Box<toml::de::Error>> {
127 toml::from_str(text).map_err(Box::new)
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn parses_a_codemod_rule() {
137 let rule = Rule::from_toml_str(
138 r#"
139 id = "destructure-default"
140 language = "typescript"
141 severity = "warning"
142 message = "Collapse optional-chain default into a destructuring bind"
143 fix = "{ $KEY: $SRC }"
144
145 [rule]
146 pattern = "$SRC?.$KEY ?? $DEFAULT"
147 "#,
148 )
149 .expect("rule parses");
150 assert_eq!(rule.id, "destructure-default");
151 assert_eq!(rule.language, "typescript");
152 assert_eq!(rule.severity, Severity::Warning);
153 assert_eq!(rule.kind(), RuleKind::Codemod);
154 assert_eq!(
155 rule.rule.resolve().unwrap(),
156 AtomicMatcher::Pattern("$SRC?.$KEY ?? $DEFAULT".into())
157 );
158 }
159
160 #[test]
161 fn severity_defaults_to_warning() {
162 let rule = Rule::from_toml_str(
163 r#"
164 id = "x"
165 language = "rust"
166 [rule]
167 kind = "macro_invocation"
168 "#,
169 )
170 .unwrap();
171 assert_eq!(rule.severity, Severity::Warning);
172 assert_eq!(rule.kind(), RuleKind::Search);
174 }
175
176 #[test]
177 fn lint_rule_has_message_no_fix() {
178 let rule = Rule::from_toml_str(
179 r#"
180 id = "todo"
181 language = "rust"
182 message = "Found a TODO"
183 [rule]
184 regex = "TODO"
185 "#,
186 )
187 .unwrap();
188 assert_eq!(rule.kind(), RuleKind::Lint);
189 assert_eq!(
190 rule.rule.resolve().unwrap(),
191 AtomicMatcher::Regex("TODO".into())
192 );
193 }
194
195 #[test]
196 fn rejects_multiple_matchers() {
197 let rule = Rule::from_toml_str(
198 r#"
199 id = "x"
200 language = "rust"
201 [rule]
202 kind = "foo"
203 regex = "bar"
204 "#,
205 )
206 .unwrap();
207 assert!(rule.rule.resolve().is_err());
208 }
209
210 #[test]
211 fn rejects_empty_matcher() {
212 let rule = Rule::from_toml_str(
213 r#"
214 id = "x"
215 language = "rust"
216 [rule]
217 "#,
218 )
219 .unwrap();
220 assert!(rule.rule.resolve().is_err());
221 }
222
223 #[test]
224 fn rejects_unknown_top_level_field() {
225 let err = Rule::from_toml_str(
226 r#"
227 id = "x"
228 language = "rust"
229 bogus = true
230 [rule]
231 kind = "foo"
232 "#,
233 );
234 assert!(err.is_err());
235 }
236}