Skip to main content

specfold_core/check/
runner.rs

1//! Runs the selected rules and orders their findings.
2
3use super::context::{Ctx, Options};
4use super::finding::Finding;
5use super::rules::catalog;
6use super::scope::{Range, Scope};
7use crate::corpus::Corpus;
8use crate::graph::Graph;
9use crate::plugins;
10use anyhow::Result;
11use rayon::prelude::*;
12
13#[derive(Debug, Clone, Default)]
14pub struct Selection {
15    /// Only these rule ids; empty means all.
16    pub only: Vec<String>,
17    /// Skip these rule ids.
18    pub skip: Vec<String>,
19}
20
21pub struct Report {
22    pub findings: Vec<Finding>,
23    pub rules_run: Vec<&'static str>,
24    pub docs_checked: usize,
25}
26
27pub fn run(c: &Corpus, g: &Graph, range: Range, opts: &Options, sel: &Selection) -> Result<Report> {
28    let t = std::time::Instant::now();
29    let scope = Scope::build(c, g, range)?;
30    let t = crate::trace::phase("scope", t);
31    let ctx = Ctx { corpus: c, graph: g, scope: &scope, opts };
32    let staged = matches!(scope.range, Range::Staged);
33    let rules: Vec<_> = catalog()
34        .into_iter()
35        .filter(|r| {
36            let id = r.info().id;
37            (sel.only.is_empty() || sel.only.iter().any(|o| o == id))
38                && !sel.skip.iter().any(|s| s == id)
39                && (!staged || r.info().on_commit)
40        })
41        .collect();
42    let mut findings: Vec<Finding> = rules
43        .par_iter()
44        .flat_map(|r| {
45            let mut out = vec![];
46            let t = std::time::Instant::now();
47            r.run(&ctx, &mut out);
48            crate::trace::rule(r.info().id, t);
49            out
50        })
51        .collect();
52    let wants_plugins = sel.only.is_empty() || sel.only.iter().any(|o| o.starts_with("x."));
53    if wants_plugins && !c.cfg.raw.plugins.is_empty() && !sel.skip.iter().any(|s| s == "x") {
54        let loaded = plugins::load(&c.cfg);
55        plugins::rules::run(&ctx, &loaded, &mut findings);
56    }
57    let t = crate::trace::phase("rules", t);
58    let mut findings = super::severity::apply(c, findings);
59    crate::trace::phase("severity", t);
60    // A secret comes first among findings of its level: it is what to fix before anything else,
61    // and an attachment that changed because of it (E1) should not be re-attached first.
62    let secret = |f: &Finding| f.rule == "U2" || (f.rule == "U1" && f.message.starts_with("[secrets]"));
63    findings.sort_by(|a, b| {
64        (a.severity, !secret(a), &a.path, a.line, &a.rule).cmp(&(b.severity, !secret(b), &b.path, b.line, &b.rule))
65    });
66    findings.dedup_by(|a, b| a.rule == b.rule && a.path == b.path && a.line == b.line && a.message == b.message);
67    let docs_checked = if scope.is_all() { c.docs.len() } else { scope.with_neighbours.len() };
68    Ok(Report { findings, rules_run: rules.iter().map(|r| r.info().id).collect(), docs_checked })
69}