Skip to main content

rust_doctor/
policy.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::str::FromStr;
4
5use clap::ValueEnum;
6use serde::{Deserialize, Serialize};
7
8use crate::configuration::WorkspaceConfiguration;
9
10mod catalog;
11#[cfg(test)]
12mod coverage;
13mod noise;
14
15pub use catalog::RuleTier;
16pub use catalog::{CatalogEntry, catalog};
17use catalog::find_in;
18pub(crate) use catalog::{
19    CARGO_DUPLICATE_MAJOR_VERSIONS, CARGO_MISSING_LOCKFILE,
20    CARGO_PATH_DEPENDENCY_OUTSIDE_WORKSPACE, CARGO_PERMISSIVE_LINT_TABLE,
21    CARGO_PERMISSIVE_RUSTFLAGS, CARGO_RELEASE_DEBUG_SYMBOLS, CARGO_TEST_ONLY_DEPENDENCY,
22    CARGO_UNBOUNDED_REGISTRY, CARGO_UNCHECKED_RELEASE_OVERFLOW, CARGO_UNPINNED_GIT,
23    CARGO_UNUSED_DEPENDENCY, CATALOG, CATEGORIES, Producer, REPO_HARDCODED_CREDENTIAL,
24    REPO_TRACKED_SECRET_FILE, REPO_UNIGNORED_BUILD_OUTPUT, RuleDefinition, SOURCE_DISABLED_TLS,
25    SOURCE_DYNAMIC_SHELL, STRUCTURE_COMPLEX_FUNCTION, STRUCTURE_CRATE_LEVEL_ALLOW,
26    STRUCTURE_DUPLICATE_FUNCTION_BODY, STRUCTURE_NEAR_DUPLICATE_FUNCTION_BODY,
27    STRUCTURE_ORPHAN_MODULE_FILE, STRUCTURE_OVERSIZED_UNIT, STRUCTURE_STACKED_ALLOW,
28    STRUCTURE_UNREASONED_ALLOW, STRUCTURE_UNREFERENCED_FEATURE, find,
29};
30pub(crate) use noise::{CorpusMeasurement, UNMEASURED_NOISE_BASIS_POINTS, corpus_measurement};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, ValueEnum)]
33#[serde(rename_all = "lowercase")]
34pub enum RuleLevel {
35    Off,
36    Warn,
37    Error,
38}
39
40impl RuleLevel {
41    pub(crate) const fn is_active(self) -> bool {
42        !matches!(self, Self::Off)
43    }
44
45    pub(crate) const fn clippy_flag(self) -> Option<&'static str> {
46        match self {
47            Self::Off => None,
48            Self::Warn | Self::Error => Some("-W"),
49        }
50    }
51
52    pub const fn as_str(self) -> &'static str {
53        match self {
54            Self::Off => "off",
55            Self::Warn => "warn",
56            Self::Error => "error",
57        }
58    }
59}
60
61impl fmt::Display for RuleLevel {
62    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63        formatter.write_str(self.as_str())
64    }
65}
66
67#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize, ValueEnum)]
68#[serde(rename_all = "lowercase")]
69pub enum BlockingLevel {
70    None,
71    #[default]
72    Error,
73    Warning,
74}
75
76impl BlockingLevel {
77    pub const fn as_str(self) -> &'static str {
78        match self {
79            Self::None => "none",
80            Self::Error => "error",
81            Self::Warning => "warning",
82        }
83    }
84}
85
86impl fmt::Display for BlockingLevel {
87    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
88        formatter.write_str(self.as_str())
89    }
90}
91
92/// The two override kinds are the same pair, a selector and a level, read from
93/// the same `KEY=LEVEL` syntax and rendered back the same way. They stay
94/// distinct types so a category selector cannot reach `with_rule_override`, and
95/// they share one body so their parsing, their rendering and their shape cannot
96/// drift apart.
97macro_rules! selector_override {
98    ($name:ident) => {
99        #[derive(Debug, Clone, PartialEq, Eq)]
100        pub struct $name {
101            selector: String,
102            level: RuleLevel,
103        }
104
105        impl $name {
106            pub fn new(selector: impl Into<String>, level: RuleLevel) -> Self {
107                Self {
108                    selector: selector.into(),
109                    level,
110                }
111            }
112
113            /// The pair validation reads, so the two kinds go through one loop.
114            fn parts(&self) -> (&str, RuleLevel) {
115                (&self.selector, self.level)
116            }
117        }
118
119        impl fmt::Display for $name {
120            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121                write!(formatter, "{}={}", self.selector, self.level)
122            }
123        }
124
125        impl FromStr for $name {
126            type Err = &'static str;
127
128            fn from_str(value: &str) -> Result<Self, Self::Err> {
129                let (selector, level) = parse_override(value)?;
130                Ok(Self::new(selector, level))
131            }
132        }
133    };
134}
135
136selector_override!(RuleOverride);
137selector_override!(CategoryOverride);
138
139fn parse_override(value: &str) -> Result<(&str, RuleLevel), &'static str> {
140    let (selector, level) = value
141        .split_once('=')
142        .ok_or("expected KEY=LEVEL with LEVEL one of: off, warn, error")?;
143    if selector.is_empty() {
144        return Err("KEY must not be empty; LEVEL must be one of: off, warn, error");
145    }
146    let level = match level {
147        "off" => RuleLevel::Off,
148        "warn" => RuleLevel::Warn,
149        "error" => RuleLevel::Error,
150        _ => return Err("LEVEL must be one of: off, warn, error"),
151    };
152    Ok((selector, level))
153}
154
155#[derive(Debug, Clone, Default, PartialEq, Eq)]
156pub(crate) struct PolicyInput {
157    rule_overrides: Vec<RuleOverride>,
158    category_overrides: Vec<CategoryOverride>,
159    blocking: Option<BlockingLevel>,
160}
161
162impl PolicyInput {
163    #[cfg(test)]
164    pub(crate) fn with_rule(mut self, selector: impl Into<String>, level: RuleLevel) -> Self {
165        self.rule_overrides.push(RuleOverride::new(selector, level));
166        self
167    }
168
169    #[cfg(test)]
170    pub(crate) fn with_category(mut self, selector: impl Into<String>, level: RuleLevel) -> Self {
171        self.category_overrides
172            .push(CategoryOverride::new(selector, level));
173        self
174    }
175
176    pub(crate) fn with_blocking(mut self, blocking: BlockingLevel) -> Self {
177        self.blocking = Some(blocking);
178        self
179    }
180
181    pub(crate) fn push_rule(&mut self, rule_override: RuleOverride) {
182        self.rule_overrides.push(rule_override);
183    }
184
185    pub(crate) fn push_category(&mut self, category_override: CategoryOverride) {
186        self.category_overrides.push(category_override);
187    }
188
189    pub(crate) fn failure_blocking(&self) -> BlockingLevel {
190        self.blocking.unwrap_or_default()
191    }
192
193    /// Reads the overrides once, against the shipped catalog. What comes back
194    /// is the only thing a plan compiles from, so a plan cannot be built out of
195    /// overrides nobody validated and validation cannot run twice.
196    pub(crate) fn validate(&self) -> Result<ValidatedPolicy<'_>, PolicyError> {
197        ValidatedPolicy::of(self, &CATALOG)
198    }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
202#[serde(rename_all = "kebab-case")]
203pub enum RuleLevelSource {
204    Default,
205    ConfigCategory,
206    ConfigRule,
207    RequestCategory,
208    RequestRule,
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
212#[serde(rename_all = "lowercase")]
213pub enum BlockingLevelSource {
214    Default,
215    Config,
216    Request,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220struct PlannedRule {
221    definition: &'static RuleDefinition,
222    level: RuleLevel,
223    source: RuleLevelSource,
224}
225
226impl PlannedRule {
227    /// A level the reader chose, at the request or through a configuration
228    /// file, rather than the one the catalog ships. It is read from the source
229    /// instead of being stored beside it: a second field for the same fact is
230    /// a second field to keep true.
231    const fn restamped(&self) -> bool {
232        !matches!(self.source, RuleLevelSource::Default)
233    }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq)]
237pub(crate) struct PolicyPlan {
238    rules: [PlannedRule; CATALOG.len()],
239    blocking: BlockingLevel,
240    blocking_source: BlockingLevelSource,
241    config_file: Option<&'static str>,
242}
243
244impl Default for PolicyPlan {
245    fn default() -> Self {
246        Self {
247            rules: CATALOG.map(|definition| PlannedRule {
248                definition,
249                level: definition.default_level,
250                source: RuleLevelSource::Default,
251            }),
252            blocking: BlockingLevel::default(),
253            blocking_source: BlockingLevelSource::Default,
254            config_file: None,
255        }
256    }
257}
258
259impl PolicyPlan {
260    #[cfg(test)]
261    pub(crate) fn compile(input: &PolicyInput) -> Result<Self, PolicyError> {
262        Ok(Self::compile_with_configuration(
263            &input.validate()?,
264            &WorkspaceConfiguration::default(),
265        ))
266    }
267
268    pub(crate) fn compile_with_configuration(
269        policy: &ValidatedPolicy<'_>,
270        configuration: &WorkspaceConfiguration,
271    ) -> Self {
272        let rules = compile_rules(&CATALOG, policy, configuration);
273        let (blocking, blocking_source) = if let Some(blocking) = policy.input.blocking {
274            (blocking, BlockingLevelSource::Request)
275        } else if let Some(blocking) = configuration.blocking {
276            (blocking, BlockingLevelSource::Config)
277        } else {
278            (BlockingLevel::default(), BlockingLevelSource::Default)
279        };
280        Self {
281            rules,
282            blocking,
283            blocking_source,
284            config_file: configuration.file_name,
285        }
286    }
287
288    fn planned(&self, id: &str) -> Option<&PlannedRule> {
289        by_id(&self.rules, id, |rule| rule.definition.id)
290    }
291
292    pub(crate) fn level(&self, id: &str) -> Option<RuleLevel> {
293        self.planned(id).map(|rule| rule.level)
294    }
295
296    pub(crate) fn is_active(&self, id: &str) -> bool {
297        self.level(id).is_some_and(RuleLevel::is_active)
298    }
299
300    pub(crate) fn restamp_level(&self, id: &str) -> Option<RuleLevel> {
301        self.planned(id)
302            .filter(|rule| rule.restamped())
303            .map(|rule| rule.level)
304    }
305
306    pub(crate) fn active_rules(
307        &self,
308        producer: Producer,
309    ) -> impl Iterator<Item = (&'static RuleDefinition, RuleLevel)> + '_ {
310        active_rules_in(&self.rules, producer)
311    }
312
313    pub(crate) const fn blocking(&self) -> BlockingLevel {
314        self.blocking
315    }
316
317    pub(crate) const fn blocking_source(&self) -> BlockingLevelSource {
318        self.blocking_source
319    }
320
321    pub(crate) const fn config_file(&self) -> Option<&'static str> {
322        self.config_file
323    }
324
325    pub(crate) fn effective_rules(
326        &self,
327    ) -> impl Iterator<Item = (&'static RuleDefinition, RuleLevel, RuleLevelSource)> + '_ {
328        self.rules
329            .iter()
330            .map(|planned| (planned.definition, planned.level, planned.source))
331    }
332}
333
334/// The rules of one producer that the plan left on.
335///
336/// A producer asks the plan once and reads the answer per finding, rather than
337/// hoisting one boolean per rule at the top of its entry point. A boolean per
338/// rule is a place the next rule has to be declared twice, once to be read and
339/// once to be counted, and the counting is what an eight-clause negated
340/// conjunction used to decide for a whole pass, silently. The set is derived
341/// from the catalog's own `producer` field, so no producer keeps a second list
342/// of the rules it owns.
343#[derive(Debug, Default, Clone, PartialEq, Eq)]
344pub(crate) struct ActiveRules {
345    on: BTreeSet<&'static str>,
346}
347
348impl ActiveRules {
349    pub(crate) fn of(plan: &PolicyPlan, producer: Producer) -> Self {
350        Self {
351            on: plan
352                .active_rules(producer)
353                .map(|(definition, _)| definition.id)
354                .collect(),
355        }
356    }
357
358    /// The set a test of one family names for itself.
359    #[cfg(test)]
360    pub(crate) fn from_rules(rules: impl IntoIterator<Item = &'static RuleDefinition>) -> Self {
361        Self {
362            on: rules.into_iter().map(|rule| rule.id).collect(),
363        }
364    }
365
366    pub(crate) fn on(&self, rule: &'static RuleDefinition) -> bool {
367        self.on.contains(rule.id)
368    }
369
370    pub(crate) fn any_of(&self, rules: &[&'static RuleDefinition]) -> bool {
371        rules.iter().any(|rule| self.on(rule))
372    }
373
374    pub(crate) fn any(&self) -> bool {
375        !self.on.is_empty()
376    }
377}
378
379fn active_rules_in(
380    rules: &[PlannedRule],
381    producer: Producer,
382) -> impl Iterator<Item = (&'static RuleDefinition, RuleLevel)> + '_ {
383    rules.iter().filter_map(move |planned| {
384        (planned.definition.producer == producer && planned.level.is_active())
385            .then_some((planned.definition, planned.level))
386    })
387}
388
389fn compile_rules<const N: usize>(
390    catalog: &[&'static RuleDefinition; N],
391    policy: &ValidatedPolicy<'_>,
392    configuration: &WorkspaceConfiguration,
393) -> [PlannedRule; N] {
394    catalog.map(|definition| {
395        let (level, source) = if let Some(level) = policy.rules.get(definition.id).copied() {
396            (level, RuleLevelSource::RequestRule)
397        } else if let Some(level) = policy.categories.get(definition.category).copied() {
398            (level, RuleLevelSource::RequestCategory)
399        } else if let Some(level) = configuration.rules.get(definition.id).copied() {
400            (level, RuleLevelSource::ConfigRule)
401        } else if let Some(level) = configuration.categories.get(definition.category).copied() {
402            (level, RuleLevelSource::ConfigCategory)
403        } else {
404            (definition.default_level, RuleLevelSource::Default)
405        };
406        PlannedRule {
407            definition,
408            level,
409            source,
410        }
411    })
412}
413
414/// Overrides read once against a catalog: every selector is spelled the way a
415/// selector may be spelled, names something that catalog knows, and appears
416/// once. A plan compiles from this and from nothing else, so the question is
417/// asked on one side of the boundary and answered on the other.
418#[derive(Debug)]
419pub(crate) struct ValidatedPolicy<'a> {
420    input: &'a PolicyInput,
421    rules: BTreeMap<&'a str, RuleLevel>,
422    categories: BTreeMap<&'a str, RuleLevel>,
423}
424
425impl<'a> ValidatedPolicy<'a> {
426    fn of(input: &'a PolicyInput, catalog: &[&RuleDefinition]) -> Result<Self, PolicyError> {
427        Ok(Self {
428            input,
429            rules: accepted(
430                input.rule_overrides.iter().map(RuleOverride::parts),
431                validate_rule_selector,
432                |selector| find_in(catalog, selector).is_some(),
433                PolicyError::unknown_rule(),
434                PolicyError::duplicate_rule(),
435            )?,
436            categories: accepted(
437                input.category_overrides.iter().map(CategoryOverride::parts),
438                validate_category_selector,
439                |selector| CATEGORIES.binary_search(&selector).is_ok(),
440                PolicyError::unknown_category(),
441                PolicyError::duplicate_category(),
442            )?,
443        })
444    }
445}
446
447/// One override list, accepted or refused. The rule list and the category list
448/// differ by how a selector is spelled, by what knows it and by which errors
449/// they carry, and by nothing else, so they go through one loop rather than
450/// through two that have to stay parallel.
451fn accepted<'a>(
452    overrides: impl Iterator<Item = (&'a str, RuleLevel)>,
453    well_formed: fn(&str) -> Result<(), PolicyError>,
454    known: impl Fn(&str) -> bool,
455    unknown: PolicyError,
456    duplicate: PolicyError,
457) -> Result<BTreeMap<&'a str, RuleLevel>, PolicyError> {
458    let mut accepted = BTreeMap::new();
459    for (selector, level) in overrides {
460        well_formed(selector)?;
461        if !known(selector) {
462            return Err(unknown);
463        }
464        if accepted.insert(selector, level).is_some() {
465            return Err(duplicate);
466        }
467    }
468    Ok(accepted)
469}
470
471/// One entry of a table sorted by identifier.
472///
473/// Four tables of this module are sorted and searched that way, and reaching
474/// the answer through `get` rather than through an index keeps the lookup total
475/// on a table whose sorting only a test holds.
476fn by_id<'a, T>(sorted: &'a [T], id: &str, key: impl Fn(&T) -> &str) -> Option<&'a T> {
477    let found = sorted.binary_search_by_key(&id, |entry| key(entry)).ok()?;
478    sorted.get(found)
479}
480
481#[derive(Debug, Clone, Copy, PartialEq, Eq)]
482pub(crate) struct PolicyError {
483    pub(crate) code: &'static str,
484    pub(crate) message: &'static str,
485}
486
487impl PolicyError {
488    const fn invalid_rule() -> Self {
489        Self {
490            code: "invalid-rule-selector",
491            message: "Invalid rule selector.",
492        }
493    }
494
495    const fn unknown_rule() -> Self {
496        Self {
497            code: "unknown-rule",
498            message: "Unknown rule selector.",
499        }
500    }
501
502    const fn duplicate_rule() -> Self {
503        Self {
504            code: "duplicate-rule-override",
505            message: "Duplicate rule override.",
506        }
507    }
508
509    const fn invalid_category() -> Self {
510        Self {
511            code: "invalid-category-selector",
512            message: "Invalid category selector.",
513        }
514    }
515
516    const fn unknown_category() -> Self {
517        Self {
518            code: "unknown-category",
519            message: "Unknown category selector.",
520        }
521    }
522
523    const fn duplicate_category() -> Self {
524        Self {
525            code: "duplicate-category-override",
526            message: "Duplicate category override.",
527        }
528    }
529}
530
531pub(crate) fn validate_rule_selector(selector: &str) -> Result<(), PolicyError> {
532    if !(1..=128).contains(&selector.len())
533        || !selector.bytes().all(|byte| {
534            byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b':')
535        })
536    {
537        return Err(PolicyError::invalid_rule());
538    }
539    Ok(())
540}
541
542pub(crate) fn validate_category_selector(selector: &str) -> Result<(), PolicyError> {
543    if !(1..=32).contains(&selector.len())
544        || !selector.bytes().all(|byte| byte.is_ascii_lowercase())
545    {
546        return Err(PolicyError::invalid_category());
547    }
548    Ok(())
549}
550
551#[cfg(test)]
552mod tests {
553    use std::collections::BTreeSet;
554
555    use super::*;
556    use crate::permutations::next_permutation;
557    use serde_json::Value;
558
559    fn oracle() -> Value {
560        serde_json::from_str(include_str!("../tests/fixtures/policy-gate/oracle.json"))
561            .expect("policy oracle should be valid JSON")
562    }
563
564    #[test]
565    fn oracle_pins_toolchain_levels_and_exit_contract() {
566        let oracle = oracle();
567        assert_eq!(
568            oracle["toolchain"]["rustc"],
569            "rustc 1.97.1 (8bab26f4f 2026-07-14)"
570        );
571        assert_eq!(
572            oracle["toolchain"]["cargo"],
573            "cargo 1.97.1 (c980f4866 2026-06-30)"
574        );
575        assert_eq!(
576            oracle["toolchain"]["clippy"],
577            "clippy 0.1.97 (8bab26f4f6 2026-07-14)"
578        );
579        assert_eq!(oracle["toolchain"]["clap"], "4.6.4");
580
581        for value in ["off", "warn", "error"] {
582            assert!(RuleLevel::from_str(value, false).is_ok(), "{value}");
583        }
584        for value in ["none", "error", "warning"] {
585            assert!(BlockingLevel::from_str(value, false).is_ok(), "{value}");
586        }
587        for value in ["allow", "warning", "deny", "forbid", "ERROR"] {
588            assert!(RuleLevel::from_str(value, false).is_err(), "{value}");
589        }
590        for value in ["off", "warn", "info", "ERROR"] {
591            assert!(BlockingLevel::from_str(value, false).is_err(), "{value}");
592        }
593
594        let exits = oracle["exit_contract"]
595            .as_array()
596            .expect("exit contract should be an array");
597        assert_eq!(exits.len(), 5);
598        let distinct: BTreeSet<_> = exits.iter().map(Value::to_string).collect();
599        assert_eq!(distinct.len(), 5);
600        assert_eq!(exits[0]["scan_status"], "complete");
601        assert_eq!(exits[1]["scan_status"], "complete");
602        assert_eq!(exits[0]["complete"], true);
603        assert_eq!(exits[1]["complete"], true);
604    }
605
606    #[test]
607    fn default_category_and_rule_precedence_are_closed_and_order_independent() {
608        let default =
609            PolicyPlan::compile(&PolicyInput::default()).expect("default policy should compile");
610        assert_eq!(default.blocking(), BlockingLevel::Error);
611        assert!(
612            CATALOG
613                .iter()
614                .all(|definition| default.level(definition.id) == Some(RuleLevel::Warn))
615        );
616
617        let category = PolicyInput::default().with_category("security", RuleLevel::Off);
618        let category = PolicyPlan::compile(&category).expect("category policy should compile");
619        assert_eq!(category.level("clippy::todo"), Some(RuleLevel::Warn));
620        assert_eq!(
621            category.level("rust_doctor::cargo::unpinned_git_dependency"),
622            Some(RuleLevel::Off)
623        );
624
625        let input = PolicyInput::default()
626            .with_rule(
627                "rust_doctor::source::dynamic_shell_command",
628                RuleLevel::Error,
629            )
630            .with_category("security", RuleLevel::Off)
631            .with_blocking(BlockingLevel::Warning);
632        let plan = PolicyPlan::compile(&input).expect("mixed policy should compile");
633        assert_eq!(
634            plan.level("rust_doctor::source::dynamic_shell_command"),
635            Some(RuleLevel::Error)
636        );
637        assert_eq!(
638            plan.level("rust_doctor::source::disabled_tls_verification"),
639            Some(RuleLevel::Off)
640        );
641        assert_eq!(plan.blocking(), BlockingLevel::Warning);
642    }
643
644    #[test]
645    fn configuration_and_request_layers_have_closed_precedence_and_provenance() {
646        let configuration = WorkspaceConfiguration {
647            file_name: Some("rust-doctor.toml"),
648            blocking: Some(BlockingLevel::Warning),
649            categories: BTreeMap::from([
650                ("correctness".to_owned(), RuleLevel::Off),
651                ("security".to_owned(), RuleLevel::Off),
652            ]),
653            rules: BTreeMap::from([
654                ("clippy::todo".to_owned(), RuleLevel::Error),
655                (
656                    "rust_doctor::source::dynamic_shell_command".to_owned(),
657                    RuleLevel::Error,
658                ),
659            ]),
660            structure: crate::structure::StructureSettings::default(),
661        };
662        let request = PolicyInput::default()
663            .with_category("correctness", RuleLevel::Warn)
664            .with_rule(
665                "rust_doctor::source::dynamic_shell_command",
666                RuleLevel::Warn,
667            );
668        let request = request.validate().expect("layered policy should validate");
669        let plan = PolicyPlan::compile_with_configuration(&request, &configuration);
670        let rules: BTreeMap<_, _> = plan
671            .effective_rules()
672            .map(|(definition, level, source)| (definition.id, (level, source)))
673            .collect();
674
675        assert_eq!(plan.config_file(), Some("rust-doctor.toml"));
676        assert_eq!(plan.blocking(), BlockingLevel::Warning);
677        assert_eq!(plan.blocking_source(), BlockingLevelSource::Config);
678        assert_eq!(
679            rules["clippy::todo"],
680            (RuleLevel::Warn, RuleLevelSource::RequestCategory)
681        );
682        assert_eq!(
683            rules["clippy::unimplemented"],
684            (RuleLevel::Warn, RuleLevelSource::RequestCategory)
685        );
686        assert_eq!(
687            rules["rust_doctor::source::dynamic_shell_command"],
688            (RuleLevel::Warn, RuleLevelSource::RequestRule)
689        );
690        assert_eq!(
691            rules["rust_doctor::source::disabled_tls_verification"],
692            (RuleLevel::Off, RuleLevelSource::ConfigCategory)
693        );
694        assert_eq!(
695            rules["clippy::dbg_macro"],
696            (RuleLevel::Warn, RuleLevelSource::Default)
697        );
698
699        let explicit = PolicyInput::default().with_blocking(BlockingLevel::None);
700        let explicit = explicit
701            .validate()
702            .expect("request blocking should validate");
703        let explicit = PolicyPlan::compile_with_configuration(&explicit, &configuration);
704        assert_eq!(explicit.blocking(), BlockingLevel::None);
705        assert_eq!(explicit.blocking_source(), BlockingLevelSource::Request);
706    }
707
708    #[test]
709    fn duplicate_unknown_and_hostile_selectors_use_closed_errors_without_echoing_input() {
710        let cases = [
711            (
712                PolicyInput::default()
713                    .with_rule("clippy::todo", RuleLevel::Warn)
714                    .with_rule("clippy::todo", RuleLevel::Error),
715                "duplicate-rule-override",
716            ),
717            (
718                PolicyInput::default()
719                    .with_category("security", RuleLevel::Warn)
720                    .with_category("security", RuleLevel::Off),
721                "duplicate-category-override",
722            ),
723            (
724                PolicyInput::default().with_rule("unknown::rule", RuleLevel::Warn),
725                "unknown-rule",
726            ),
727            (
728                PolicyInput::default().with_category("style", RuleLevel::Warn),
729                "unknown-category",
730            ),
731            (
732                PolicyInput::default().with_rule("bad/\u{001b}[31mselector", RuleLevel::Warn),
733                "invalid-rule-selector",
734            ),
735            (
736                PolicyInput::default().with_category("", RuleLevel::Warn),
737                "invalid-category-selector",
738            ),
739            (
740                PolicyInput::default().with_rule("a".repeat(129), RuleLevel::Warn),
741                "invalid-rule-selector",
742            ),
743            (
744                PolicyInput::default().with_category("a".repeat(33), RuleLevel::Warn),
745                "invalid-category-selector",
746            ),
747        ];
748
749        for (input, code) in cases {
750            let error = PolicyPlan::compile(&input).expect_err("invalid policy should fail");
751            assert_eq!(error.code, code);
752            assert!(!error.message.contains('\u{001b}'));
753            assert!(!error.message.contains('/'));
754            assert!(error.message.len() < 64);
755        }
756    }
757
758    #[test]
759    fn twenty_override_orders_compile_to_the_same_plan() {
760        #[derive(Clone, Copy)]
761        enum Override {
762            Rule(&'static str, RuleLevel),
763            Category(&'static str, RuleLevel),
764        }
765
766        let overrides = [
767            Override::Category("security", RuleLevel::Off),
768            Override::Category("correctness", RuleLevel::Error),
769            Override::Rule(
770                "rust_doctor::source::dynamic_shell_command",
771                RuleLevel::Error,
772            ),
773            Override::Rule("clippy::todo", RuleLevel::Warn),
774        ];
775        let mut expected_plan = None;
776        let mut orders = BTreeSet::new();
777        let mut order = [0, 1, 2, 3];
778        for _ in 0..20 {
779            assert!(orders.insert(order));
780            let mut input = PolicyInput::default();
781            for index in order {
782                input = match overrides[index] {
783                    Override::Rule(selector, level) => input.with_rule(selector, level),
784                    Override::Category(selector, level) => input.with_category(selector, level),
785                };
786            }
787            let plan = PolicyPlan::compile(&input).expect("permuted policy should compile");
788            match &expected_plan {
789                Some(expected) => assert_eq!(&plan, expected),
790                None => expected_plan = Some(plan),
791            }
792
793            assert!(
794                next_permutation(&mut order),
795                "twenty permutations are below the full permutation count"
796            );
797        }
798        assert_eq!(orders.len(), 20);
799    }
800}