use crate::{RuleConfig, Severity};
use ast_grep_core::language::Language;
use globset::{Glob, GlobSet, GlobSetBuilder};
use std::path::Path;
pub struct RuleBucket<L: Language> {
rules: Vec<RuleConfig<L>>,
lang: L,
}
impl<L: Language> RuleBucket<L> {
fn new(lang: L) -> Self {
Self {
rules: vec![],
lang,
}
}
pub fn add(&mut self, rule: RuleConfig<L>) {
self.rules.push(rule);
}
}
struct ContingentRule<L: Language> {
rule: RuleConfig<L>,
files_globs: Option<GlobSet>,
ignore_globs: Option<GlobSet>,
}
fn build_glob_set(paths: &Vec<String>) -> Result<GlobSet, globset::Error> {
let mut builder = GlobSetBuilder::new();
for path in paths {
builder.add(Glob::new(path)?);
}
builder.build()
}
impl<L> TryFrom<RuleConfig<L>> for ContingentRule<L>
where
L: Language,
{
type Error = globset::Error;
fn try_from(rule: RuleConfig<L>) -> Result<Self, Self::Error> {
let files_globs = rule.files.as_ref().map(build_glob_set).transpose()?;
let ignore_globs = rule.ignores.as_ref().map(build_glob_set).transpose()?;
Ok(Self {
rule,
files_globs,
ignore_globs,
})
}
}
impl<L: Language> ContingentRule<L> {
pub fn matches_path<P: AsRef<Path>>(&self, path: P) -> bool {
if let Some(ignore_globs) = &self.ignore_globs {
if ignore_globs.is_match(&path) {
return false;
}
}
if let Some(files_globs) = &self.files_globs {
return files_globs.is_match(path);
}
true
}
}
pub struct RuleCollection<L: Language + Eq> {
tenured: Vec<RuleBucket<L>>,
contingent: Vec<ContingentRule<L>>,
}
impl<L: Language + Eq> RuleCollection<L> {
pub fn try_new(configs: Vec<RuleConfig<L>>) -> Result<Self, globset::Error> {
let mut tenured = vec![];
let mut contingent = vec![];
for config in configs {
if matches!(config.severity, Severity::Off) {
continue;
} else if config.files.is_none() && config.ignores.is_none() {
Self::add_tenured_rule(&mut tenured, config);
} else {
contingent.push(ContingentRule::try_from(config)?);
}
}
Ok(Self {
tenured,
contingent,
})
}
pub fn for_path<P: AsRef<Path>>(&self, path: P) -> Vec<&RuleConfig<L>> {
let mut all_rules = vec![];
let Some(lang) = L::from_path(path.as_ref()) else {
return vec![];
};
for rule in &self.tenured {
if rule.lang == lang {
all_rules = rule.rules.iter().collect();
break;
}
}
all_rules.extend(self.contingent.iter().filter_map(|cont| {
if cont.rule.language == lang && cont.matches_path(path.as_ref()) {
Some(&cont.rule)
} else {
None
}
}));
all_rules
}
pub fn get_rule(&self, id: &str) -> Option<&RuleConfig<L>> {
for rule in &self.tenured {
for r in &rule.rules {
if r.id == id {
return Some(r);
}
}
}
for rule in &self.contingent {
if rule.rule.id == id {
return Some(&rule.rule);
}
}
None
}
fn add_tenured_rule(tenured: &mut Vec<RuleBucket<L>>, rule: RuleConfig<L>) {
let lang = rule.language.clone();
for bucket in tenured.iter_mut() {
if bucket.lang == lang {
bucket.add(rule);
return;
}
}
let mut bucket = RuleBucket::new(lang);
bucket.add(rule);
tenured.push(bucket);
}
}
impl<L: Language + Eq> Default for RuleCollection<L> {
fn default() -> Self {
Self {
tenured: vec![],
contingent: vec![],
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::from_yaml_string;
use crate::referent_rule::GlobalRules;
use crate::test::TypeScript;
fn make_rule(files: &str) -> RuleCollection<TypeScript> {
let globals = GlobalRules::default();
let rule_config = from_yaml_string(
&format!(
r"
id: test
message: test rule
severity: info
language: Tsx
rule:
all: []
{files}"
),
&globals,
)
.unwrap()
.pop()
.unwrap();
RuleCollection::try_new(vec![rule_config]).expect("should parse")
}
fn assert_match_path(collection: &RuleCollection<TypeScript>, path: &str) {
let rules = collection.for_path(path);
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].id, "test");
}
fn assert_ignore_path(collection: &RuleCollection<TypeScript>, path: &str) {
let rules = collection.for_path(path);
assert!(rules.is_empty());
}
#[test]
fn test_ignore_rule() {
let src = r#"
ignores:
- ./manage.py
- "**/test*"
"#;
let collection = make_rule(src);
assert_ignore_path(&collection, "./manage.py");
assert_ignore_path(&collection, "./src/test.py");
assert_match_path(&collection, "./src/app.py");
}
#[test]
fn test_files_rule() {
let src = r#"
files:
- ./manage.py
- "**/test*"
"#;
let collection = make_rule(src);
assert_match_path(&collection, "./manage.py");
assert_match_path(&collection, "./src/test.py");
assert_ignore_path(&collection, "./src/app.py");
}
#[test]
fn test_files_with_ignores_rule() {
let src = r#"
files:
- ./src/**/*.py
ignores:
- ./src/excluded/*.py
"#;
let collection = make_rule(src);
assert_match_path(&collection, "./src/test.py");
assert_match_path(&collection, "./src/some_folder/test.py");
assert_ignore_path(&collection, "./src/excluded/app.py");
}
#[test]
fn test_rule_collection_get_contingent_rule() {
let src = r#"
files:
- ./manage.py
- "**/test*"
"#;
let collection = make_rule(src);
assert!(collection.get_rule("test").is_some());
}
#[test]
fn test_rule_collection_get_tenured_rule() {
let src = r#""#;
let collection = make_rule(src);
assert!(collection.get_rule("test").is_some());
}
#[test]
#[ignore]
fn test_rules_for_path() {
todo!()
}
}