ast_grep_config/
rule_collection.rs

1use crate::{RuleConfig, Severity};
2use ast_grep_core::language::Language;
3use globset::{Glob, GlobSet, GlobSetBuilder};
4use std::path::Path;
5
6/// RuleBucket stores rules of the same language id.
7/// Rules for different language will stay in separate buckets.
8pub 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
69/// A collection of rules to run one round of scanning.
70/// Rules will be grouped together based on their language, path globbing and pattern rule.
71pub struct RuleCollection<L: Language + Eq> {
72  // use vec since we don't have many languages
73  /// a list of rule buckets grouped by languages.
74  /// Tenured rules will always run against a file of that language type.
75  tenured: Vec<RuleBucket<L>>,
76  /// contingent rules will run against a file if it matches file/ignore glob.
77  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}