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
92macro_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 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 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 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#[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 #[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#[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
447fn 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
471fn 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}