ast_grep_config/
rule_collection.rs1use crate::{RuleConfig, Severity};
2use ast_grep_core::language::Language;
3use globset::{Glob, GlobSet, GlobSetBuilder};
4use std::path::Path;
5
6pub struct RuleBucket<L: Language> {
9 rules: Vec<RuleConfig<L>>,
10 lang: L,
11}
12
13impl<L: Language> RuleBucket<L> {
14 fn new(lang: L) -> Self {
15 Self {
16 rules: vec![],
17 lang,
18 }
19 }
20 pub fn add(&mut self, rule: RuleConfig<L>) {
21 self.rules.push(rule);
22 }
23}
24
25struct ContingentRule<L: Language> {
26 rule: RuleConfig<L>,
27 files_globs: Option<GlobSet>,
28 ignore_globs: Option<GlobSet>,
29}
30
31fn build_glob_set(paths: &Vec<String>) -> Result<GlobSet, globset::Error> {
32 let mut builder = GlobSetBuilder::new();
33 for path in paths {
34 builder.add(Glob::new(path)?);
35 }
36 builder.build()
37}
38
39impl<L> TryFrom<RuleConfig<L>> for ContingentRule<L>
40where
41 L: Language,
42{
43 type Error = globset::Error;
44 fn try_from(rule: RuleConfig<L>) -> Result<Self, Self::Error> {
45 let files_globs = rule.files.as_ref().map(build_glob_set).transpose()?;
46 let ignore_globs = rule.ignores.as_ref().map(build_glob_set).transpose()?;
47 Ok(Self {
48 rule,
49 files_globs,
50 ignore_globs,
51 })
52 }
53}
54
55impl<L: Language> ContingentRule<L> {
56 pub fn matches_path<P: AsRef<Path>>(&self, path: P) -> bool {
57 if let Some(ignore_globs) = &self.ignore_globs {
58 if ignore_globs.is_match(&path) {
59 return false;
60 }
61 }
62 if let Some(files_globs) = &self.files_globs {
63 return files_globs.is_match(path);
64 }
65 true
66 }
67}
68
69pub struct RuleCollection<L: Language + Eq> {
72 tenured: Vec<RuleBucket<L>>,
76 contingent: Vec<ContingentRule<L>>,
78}
79
80impl<L: Language + Eq> RuleCollection<L> {
81 pub fn try_new(configs: Vec<RuleConfig<L>>) -> Result<Self, globset::Error> {
82 let mut tenured = vec![];
83 let mut contingent = vec![];
84 for config in configs {
85 if matches!(config.severity, Severity::Off) {
86 continue;
87 } else if config.files.is_none() && config.ignores.is_none() {
88 Self::add_tenured_rule(&mut tenured, config);
89 } else {
90 contingent.push(ContingentRule::try_from(config)?);
91 }
92 }
93 Ok(Self {
94 tenured,
95 contingent,
96 })
97 }
98
99 pub fn get_rule_from_lang(&self, path: &Path, lang: L) -> Vec<&RuleConfig<L>> {
100 let mut all_rules = vec![];
101 for rule in &self.tenured {
102 if rule.lang == lang {
103 all_rules = rule.rules.iter().collect();
104 break;
105 }
106 }
107 all_rules.extend(self.contingent.iter().filter_map(|cont| {
108 if cont.rule.language == lang && cont.matches_path(path) {
109 Some(&cont.rule)
110 } else {
111 None
112 }
113 }));
114 all_rules
115 }
116
117 pub fn for_path<P: AsRef<Path>>(&self, path: P) -> Vec<&RuleConfig<L>> {
118 let path = path.as_ref();
119 let Some(lang) = L::from_path(path) else {
120 return vec![];
121 };
122 let mut ret = self.get_rule_from_lang(path, lang);
123 ret.sort_unstable_by_key(|r| &r.id);
124 ret
125 }
126
127 pub fn get_rule(&self, id: &str) -> Option<&RuleConfig<L>> {
128 for rule in &self.tenured {
129 for r in &rule.rules {
130 if r.id == id {
131 return Some(r);
132 }
133 }
134 }
135 for rule in &self.contingent {
136 if rule.rule.id == id {
137 return Some(&rule.rule);
138 }
139 }
140 None
141 }
142
143 pub fn total_rule_count(&self) -> usize {
144 let mut ret = self.tenured.iter().map(|bucket| bucket.rules.len()).sum();
145 ret += self.contingent.len();
146 ret
147 }
148
149 pub fn for_each_rule(&self, mut f: impl FnMut(&RuleConfig<L>)) {
150 for bucket in &self.tenured {
151 for rule in &bucket.rules {
152 f(rule);
153 }
154 }
155 for rule in &self.contingent {
156 f(&rule.rule);
157 }
158 }
159
160 fn add_tenured_rule(tenured: &mut Vec<RuleBucket<L>>, rule: RuleConfig<L>) {
161 let lang = rule.language.clone();
162 for bucket in tenured.iter_mut() {
163 if bucket.lang == lang {
164 bucket.add(rule);
165 return;
166 }
167 }
168 let mut bucket = RuleBucket::new(lang);
169 bucket.add(rule);
170 tenured.push(bucket);
171 }
172}
173
174impl<L: Language + Eq> Default for RuleCollection<L> {
175 fn default() -> Self {
176 Self {
177 tenured: vec![],
178 contingent: vec![],
179 }
180 }
181}
182
183#[cfg(test)]
184mod test {
185 use super::*;
186 use crate::from_yaml_string;
187 use crate::test::TypeScript;
188 use crate::GlobalRules;
189
190 fn make_rule(files: &str) -> RuleCollection<TypeScript> {
191 let globals = GlobalRules::default();
192 let rule_config = from_yaml_string(
193 &format!(
194 r"
195id: test
196message: test rule
197severity: info
198language: Tsx
199rule:
200 all: [kind: number]
201{files}"
202 ),
203 &globals,
204 )
205 .unwrap()
206 .pop()
207 .unwrap();
208 RuleCollection::try_new(vec![rule_config]).expect("should parse")
209 }
210
211 fn assert_match_path(collection: &RuleCollection<TypeScript>, path: &str) {
212 let rules = collection.for_path(path);
213 assert_eq!(rules.len(), 1);
214 assert_eq!(rules[0].id, "test");
215 }
216
217 fn assert_ignore_path(collection: &RuleCollection<TypeScript>, path: &str) {
218 let rules = collection.for_path(path);
219 assert!(rules.is_empty());
220 }
221
222 #[test]
223 fn test_ignore_rule() {
224 let src = r#"
225ignores:
226 - ./manage.py
227 - "**/test*"
228"#;
229 let collection = make_rule(src);
230 assert_ignore_path(&collection, "./manage.py");
231 assert_ignore_path(&collection, "./src/test.py");
232 assert_match_path(&collection, "./src/app.py");
233 }
234
235 #[test]
236 fn test_files_rule() {
237 let src = r#"
238files:
239 - ./manage.py
240 - "**/test*"
241"#;
242 let collection = make_rule(src);
243 assert_match_path(&collection, "./manage.py");
244 assert_match_path(&collection, "./src/test.py");
245 assert_ignore_path(&collection, "./src/app.py");
246 }
247
248 #[test]
249 fn test_files_with_ignores_rule() {
250 let src = r#"
251files:
252 - ./src/**/*.py
253ignores:
254 - ./src/excluded/*.py
255"#;
256 let collection = make_rule(src);
257 assert_match_path(&collection, "./src/test.py");
258 assert_match_path(&collection, "./src/some_folder/test.py");
259 assert_ignore_path(&collection, "./src/excluded/app.py");
260 }
261
262 #[test]
263 fn test_rule_collection_get_contingent_rule() {
264 let src = r#"
265files:
266 - ./manage.py
267 - "**/test*"
268"#;
269 let collection = make_rule(src);
270 assert!(collection.get_rule("test").is_some());
271 }
272
273 #[test]
274 fn test_rule_collection_get_tenured_rule() {
275 let src = r#""#;
276 let collection = make_rule(src);
277 assert!(collection.get_rule("test").is_some());
278 }
279
280 #[test]
281 #[ignore]
282 fn test_rules_for_path() {
283 todo!()
284 }
285}