use rayon::prelude::*;
use scraps_libs::model::{scrap::Scrap, tags::Tags};
use crate::{error::ScrapsResult, usecase::build::model::backlinks_map::BacklinksMap};
use super::{
rule::{LintRule, LintRuleName, LintWarning},
rules::{
broken_heading_ref::BrokenHeadingRefRule, broken_link::BrokenLinkRule,
dead_end::DeadEndRule, lonely::LonelyRule, overlinking::OverlinkingRule,
self_link::SelfLinkRule, source_has_page::SourceHasPageRule,
unlinkable_key::UnlinkableKeyRule,
},
};
pub struct LintUsecase;
impl LintUsecase {
pub fn new() -> LintUsecase {
LintUsecase
}
pub fn execute(
&self,
scraps: &[Scrap],
rule_names: &[LintRuleName],
extra_rules: Vec<Box<dyn LintRule>>,
) -> ScrapsResult<Vec<LintWarning>> {
let backlinks_map = BacklinksMap::new(scraps);
let tags = Tags::new(scraps);
let mut rules: Vec<Box<dyn LintRule>> = vec![
Box::new(DeadEndRule),
Box::new(LonelyRule),
Box::new(SelfLinkRule),
Box::new(OverlinkingRule),
Box::new(BrokenLinkRule),
Box::new(BrokenHeadingRefRule),
Box::new(UnlinkableKeyRule),
Box::new(SourceHasPageRule),
];
if !rule_names.is_empty() {
rules.extend(extra_rules);
rules.retain(|r| rule_names.contains(&r.name()));
}
let warnings: Vec<LintWarning> = rules
.par_iter()
.flat_map(|rule| rule.check(scraps, &backlinks_map, &tags))
.collect();
Ok(warnings)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_rules_run_and_aggregate() {
let scraps = vec![
Scrap::new("no_links", &None, "plain text"),
Scrap::new("self_linker", &None, "[[self_linker]] [[no_links]]"),
Scrap::new("overlinker", &None, "[[no_links]] [[no_links]]"),
Scrap::new("linker_to_unknown", &None, "[[unknown]]"),
Scrap::new("heading_referrer", &None, "[[no_links#missing]]"),
];
let usecase = LintUsecase::new();
let warnings = usecase.execute(&scraps, &[], Vec::new()).unwrap();
let rule_names: Vec<&LintRuleName> = warnings.iter().map(|w| &w.rule_name).collect();
assert!(rule_names.contains(&&LintRuleName::DeadEnd));
assert!(rule_names.contains(&&LintRuleName::Lonely));
assert!(rule_names.contains(&&LintRuleName::SelfLink));
assert!(rule_names.contains(&&LintRuleName::Overlinking));
assert!(rule_names.contains(&&LintRuleName::BrokenLink));
assert!(rule_names.contains(&&LintRuleName::BrokenHeadingRef));
}
#[test]
fn clean_project_no_warnings() {
let scraps = vec![
Scrap::new("a", &None, "[[b]] #[[shared_tag]]"),
Scrap::new("b", &None, "[[a]] #[[shared_tag]]"),
];
let usecase = LintUsecase::new();
let warnings = usecase.execute(&scraps, &[], Vec::new()).unwrap();
assert!(warnings.is_empty());
}
#[test]
fn empty_project_no_errors() {
let usecase = LintUsecase::new();
let warnings = usecase.execute(&[], &[], Vec::new()).unwrap();
assert!(warnings.is_empty());
}
#[test]
fn filter_by_specific_rule() {
let scraps = vec![
Scrap::new("no_links", &None, "plain text"),
Scrap::new("self_linker", &None, "[[self_linker]] [[no_links]]"),
];
let usecase = LintUsecase::new();
let warnings = usecase
.execute(&scraps, &[LintRuleName::DeadEnd], Vec::new())
.unwrap();
assert!(
warnings
.iter()
.all(|w| w.rule_name == LintRuleName::DeadEnd)
);
assert!(!warnings.is_empty());
}
#[test]
fn default_excludes_opt_in_rules() {
use crate::usecase::lint::rules::stale_by_git::StaleByGitRule;
use scraps_libs::git::tests::GitCommandTest;
use std::path::PathBuf;
let scraps = vec![
Scrap::new("a", &None, "[[b]]"),
Scrap::new("b", &None, "[[a]]"),
];
let stale_rule = StaleByGitRule {
git_command: GitCommandTest::new(),
scraps_dir: PathBuf::from("/tmp"),
threshold_days: 1,
now_ts: 1_700_000_000,
};
let usecase = LintUsecase::new();
let warnings = usecase
.execute(&scraps, &[], vec![Box::new(stale_rule)])
.unwrap();
assert!(
warnings
.iter()
.all(|w| w.rule_name != LintRuleName::StaleByGit)
);
}
#[test]
fn unportable_key_is_opt_in() {
use crate::usecase::lint::rules::unportable_key::UnportableKeyRule;
let scraps = vec"),
Scrap::new("b", &None, "[a](a%3F.md)"),
];
let usecase = LintUsecase::new();
let default = usecase
.execute(&scraps, &[], vec![Box::new(UnportableKeyRule)])
.unwrap();
assert!(default.is_empty());
let selected = usecase
.execute(
&scraps,
&[LintRuleName::UnportableKey],
vec![Box::new(UnportableKeyRule)],
)
.unwrap();
assert_eq!(selected.len(), 1);
}
#[test]
fn extra_rule_runs_when_explicitly_selected() {
use crate::usecase::lint::rules::stale_by_git::StaleByGitRule;
use scraps_libs::git::tests::GitCommandTest;
use std::path::PathBuf;
let scraps = vec![
Scrap::new("a", &None, "[[b]]"),
Scrap::new("b", &None, "[[a]]"),
];
let stale_rule = StaleByGitRule {
git_command: GitCommandTest::new(),
scraps_dir: PathBuf::from("/tmp"),
threshold_days: 1,
now_ts: 1_700_000_000,
};
let usecase = LintUsecase::new();
let warnings = usecase
.execute(
&scraps,
&[LintRuleName::StaleByGit],
vec![Box::new(stale_rule)],
)
.unwrap();
assert!(!warnings.is_empty());
assert!(
warnings
.iter()
.all(|w| w.rule_name == LintRuleName::StaleByGit)
);
}
}