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}