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}