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    // Skipping wins over selecting. Asking for a rule and excluding it in the
27    // same breath is a contradiction, and the safer reading of a contradiction
28    // is the narrower one.
29    pub fn includes(&self, name: &str) -> bool {
30        if self.skipped.iter().any(|skipped| skipped == name) {
31            return false;
32        }
33        self.selected.is_empty() || self.selected.iter().any(|selected| selected == name)
34    }
35
36    // Distinct from includes(), because "this rule was asked for by name" and
37    // "this rule is in the set" differ for a rule that cannot run without
38    // configuration. Asking for the header rule without a header file is an
39    // error; not asking for it is an omission.
40    pub fn selects_explicitly(&self, name: &str) -> bool {
41        self.selected.iter().any(|selected| selected == name)
42    }
43
44    // A misspelled name is an error rather than a rule that quietly matches
45    // nothing. `--skip test-file-strucutre` that silently skipped nothing would
46    // look exactly like a run that worked.
47    pub fn unknown_in(&self, known: &[&str]) -> Vec<String> {
48        self.selected
49            .iter()
50            .chain(self.skipped.iter())
51            .filter(|name| !known.contains(&name.as_str()))
52            .cloned()
53            .collect()
54    }
55}