Skip to main content

stern4rust/settings/
rule_selection.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5// Which rules a run applies.
6//
7// The default is everything, because a tool that does nothing until it is
8// configured is a tool nobody switches on. The switches exist for adoption: a
9// repository facing two hundred offences cannot gate on all five rules today,
10// but it can gate on one of them today and the rest as it goes.
11//
12// Naming a rule with --rule makes the selection a whitelist; --skip subtracts
13// from whatever is left. That is the shape clippy, ruff and eslint converge on,
14// so nobody has to learn this one.
15#[derive(Debug, Clone, Default, PartialEq, Eq)]
16pub struct RuleSelection {
17    selected: Vec<String>,
18    skipped: Vec<String>,
19}
20
21impl RuleSelection {
22    pub fn new(selected: Vec<String>, skipped: Vec<String>) -> Self {
23        Self { selected, skipped }
24    }
25
26    // The same selection, standing down on these as well.
27    //
28    // Used to fold every package section's skips into the run-level answer, so
29    // the report cannot claim a rule applied when one package stood it down.
30    pub fn also_skipping(&self, more: &[String]) -> Self {
31        let mut skipped = self.skipped.clone();
32        skipped.extend(more.iter().cloned());
33        skipped.sort();
34        skipped.dedup();
35        Self {
36            selected: self.selected.clone(),
37            skipped,
38        }
39    }
40
41    // Skipping wins over selecting. Asking for a rule and excluding it in the
42    // same breath is a contradiction, and the safer reading of a contradiction
43    // is the narrower one.
44    pub fn includes(&self, name: &str) -> bool {
45        if self.skipped.iter().any(|skipped| skipped == name) {
46            return false;
47        }
48        self.selected.is_empty() || self.selected.iter().any(|selected| selected == name)
49    }
50
51    // Distinct from includes(), because "this rule was asked for by name" and
52    // "this rule is in the set" differ for a rule that cannot run without
53    // configuration. Asking for the header rule without a header file is an
54    // error; not asking for it is an omission.
55    pub fn selects_explicitly(&self, name: &str) -> bool {
56        self.selected.iter().any(|selected| selected == name)
57    }
58
59    // A misspelled name is an error rather than a rule that quietly matches
60    // nothing. `--skip test-file-strucutre` that silently skipped nothing would
61    // look exactly like a run that worked.
62    pub fn unknown_in(&self, known: &[&str]) -> Vec<String> {
63        self.selected
64            .iter()
65            .chain(self.skipped.iter())
66            .filter(|name| !known.contains(&name.as_str()))
67            .cloned()
68            .collect()
69    }
70}