1use std::sync::LazyLock;
2
3use crate::rule::Rule;
4
5use super::flavor::normalize_key;
6
7static DEFAULT_REGISTRY: LazyLock<RuleRegistry> = LazyLock::new(|| {
13 let default_config = super::types::Config::default();
14 let rules = crate::rules::all_rules(&default_config);
15 RuleRegistry::from_rules(&rules)
16});
17
18pub fn default_registry() -> &'static RuleRegistry {
24 &DEFAULT_REGISTRY
25}
26
27pub struct RuleRegistry {
29 pub rule_schemas: std::collections::BTreeMap<String, toml::map::Map<String, toml::Value>>,
31 pub rule_aliases: std::collections::BTreeMap<String, std::collections::HashMap<String, String>>,
33}
34
35impl RuleRegistry {
36 pub fn from_rules(rules: &[Box<dyn Rule>]) -> Self {
38 let mut rule_schemas = std::collections::BTreeMap::new();
39 let mut rule_aliases = std::collections::BTreeMap::new();
40
41 for rule in rules {
42 let norm_name = if let Some((name, toml::Value::Table(mut table))) = rule.config_schema() {
43 let norm_name = normalize_key(&name); for key in rule.polymorphic_config_keys() {
49 table.insert(
50 (*key).to_string(),
51 crate::rule_config_serde::polymorphic_sentinel_value(),
52 );
53 }
54 rule_schemas.insert(norm_name.clone(), table);
55 norm_name
56 } else {
57 let norm_name = normalize_key(rule.name()); rule_schemas.insert(norm_name.clone(), toml::map::Map::new());
59 norm_name
60 };
61
62 if let Some(aliases) = rule.config_aliases() {
64 rule_aliases.insert(norm_name, aliases);
65 }
66 }
67
68 RuleRegistry {
69 rule_schemas,
70 rule_aliases,
71 }
72 }
73
74 pub fn rule_names(&self) -> std::collections::BTreeSet<String> {
76 self.rule_schemas.keys().cloned().collect()
77 }
78
79 pub fn config_keys_for(&self, rule: &str) -> Option<std::collections::BTreeSet<String>> {
81 self.rule_schemas.get(rule).map(|schema| {
82 let mut all_keys = std::collections::BTreeSet::new();
83
84 all_keys.insert("severity".to_string());
86 all_keys.insert("enabled".to_string());
87
88 for key in schema.keys() {
90 all_keys.insert(key.clone());
91 }
92
93 for key in schema.keys() {
95 all_keys.insert(key.replace('_', "-"));
97 all_keys.insert(key.replace('-', "_"));
99 all_keys.insert(normalize_key(key));
101 }
102
103 if let Some(aliases) = self.rule_aliases.get(rule) {
105 for alias_key in aliases.keys() {
106 all_keys.insert(alias_key.clone());
107 all_keys.insert(alias_key.replace('_', "-"));
109 all_keys.insert(alias_key.replace('-', "_"));
110 all_keys.insert(normalize_key(alias_key));
111 }
112 }
113
114 all_keys
115 })
116 }
117
118 pub fn canonical_config_key(&self, rule: &str, key: &str) -> Option<&str> {
125 let schema = self.rule_schemas.get(rule)?;
126
127 if let Some(aliases) = self.rule_aliases.get(rule)
129 && let Some(canonical_key) = aliases.get(key)
130 && let Some((schema_key, _)) = schema.get_key_value(canonical_key)
131 {
132 return Some(schema_key);
133 }
134
135 if let Some((schema_key, _)) = schema.get_key_value(key) {
137 return Some(schema_key);
138 }
139
140 let key_variants = [
142 key.replace('-', "_"), key.replace('_', "-"), normalize_key(key), ];
146
147 for variant in &key_variants {
148 if let Some((schema_key, _)) = schema.get_key_value(variant) {
149 return Some(schema_key);
150 }
151 }
152
153 None
154 }
155
156 pub fn expected_value_for(&self, rule: &str, key: &str) -> Option<&toml::Value> {
162 let schema = self.rule_schemas.get(rule)?;
163 let canonical = self.canonical_config_key(rule, key)?;
164 filter_type_check_sentinels(schema.get(canonical)?)
165 }
166
167 pub fn resolve_rule_name(&self, name: &str) -> Option<String> {
174 let normalized = normalize_key(name);
176 if self.rule_schemas.contains_key(&normalized) {
177 return Some(normalized);
178 }
179
180 resolve_rule_name_alias(name).map(std::string::ToString::to_string)
182 }
183}
184
185fn filter_type_check_sentinels(value: &toml::Value) -> Option<&toml::Value> {
189 if crate::rule_config_serde::is_nullable_sentinel(value) || crate::rule_config_serde::is_polymorphic_sentinel(value)
190 {
191 None
192 } else {
193 Some(value)
194 }
195}
196
197pub struct StaticMap {
204 entries: &'static [(&'static str, &'static str)],
205}
206
207impl StaticMap {
208 const fn new(entries: &'static [(&'static str, &'static str)]) -> Self {
209 Self { entries }
210 }
211
212 pub fn get(&self, key: &str) -> Option<&'static str> {
214 self.entries
215 .binary_search_by_key(&key, |(k, _)| k)
216 .ok()
217 .map(|i| self.entries[i].1)
218 }
219
220 pub fn keys(&self) -> impl Iterator<Item = &'static str> {
222 self.entries.iter().map(|(k, _)| *k)
223 }
224
225 pub fn entries(&self) -> impl Iterator<Item = (&'static str, &'static str)> {
227 self.entries.iter().copied()
228 }
229
230 #[cfg(test)]
235 fn is_sorted_by_key(&self) -> bool {
236 self.entries.windows(2).all(|w| w[0].0 < w[1].0)
237 }
238}
239
240pub static RULE_ALIAS_MAP: StaticMap = StaticMap::new(&[
246 ("BLANK-LINE-AFTER-FRONTMATTER", "MD071"),
247 ("BLANKS-AROUND-FENCES", "MD031"),
248 ("BLANKS-AROUND-HEADINGS", "MD022"),
249 ("BLANKS-AROUND-HORIZONTAL-RULES", "MD065"),
250 ("BLANKS-AROUND-LISTS", "MD032"),
251 ("BLANKS-AROUND-TABLES", "MD058"),
252 ("CHUNK-LABEL-SPACES", "MD079"),
253 ("CODE-BLOCK-STYLE", "MD046"),
254 ("CODE-FENCE-STYLE", "MD048"),
255 ("COMMANDS-SHOW-OUTPUT", "MD014"),
256 ("DESCRIPTIVE-LINK-TEXT", "MD059"),
257 ("EMPHASIS-STYLE", "MD049"),
258 ("EMPTY-FOOTNOTE-DEFINITION", "MD068"),
259 ("EXISTING-RELATIVE-LINKS", "MD057"),
260 ("FENCED-CODE-LANGUAGE", "MD040"),
261 ("FIRST-LINE-H1", "MD041"),
262 ("FIRST-LINE-HEADING", "MD041"),
263 ("FOOTNOTE-DEFINITION-ORDER", "MD067"),
264 ("FOOTNOTE-VALIDATION", "MD066"),
265 ("FORBIDDEN-TERMS", "MD061"),
266 ("FRONTMATTER-KEY-SORT", "MD072"),
267 ("HEADING-ANCHOR-COLLISION", "MD080"),
268 ("HEADING-CAPITALIZATION", "MD063"),
269 ("HEADING-INCREMENT", "MD001"),
270 ("HEADING-START-LEFT", "MD023"),
271 ("HEADING-STYLE", "MD003"),
272 ("HR-STYLE", "MD035"),
273 ("INVISIBLE-CHARACTERS", "MD084"),
274 ("LINE-LENGTH", "MD013"),
275 ("LINK-DESTINATION-WHITESPACE", "MD062"),
276 ("LINK-FRAGMENTS", "MD051"),
277 ("LINK-IMAGE-REFERENCE-DEFINITIONS", "MD053"),
278 ("LINK-IMAGE-STYLE", "MD054"),
279 ("LIST-CONTINUATION-INDENT", "MD077"),
280 ("LIST-INDENT", "MD005"),
281 ("LIST-ITEM-SPACING", "MD076"),
282 ("LIST-MARKER-SPACE", "MD030"),
283 ("MD001", "MD001"),
284 ("MD003", "MD003"),
285 ("MD004", "MD004"),
286 ("MD005", "MD005"),
287 ("MD007", "MD007"),
288 ("MD009", "MD009"),
289 ("MD010", "MD010"),
290 ("MD011", "MD011"),
291 ("MD012", "MD012"),
292 ("MD013", "MD013"),
293 ("MD014", "MD014"),
294 ("MD018", "MD018"),
295 ("MD019", "MD019"),
296 ("MD020", "MD020"),
297 ("MD021", "MD021"),
298 ("MD022", "MD022"),
299 ("MD023", "MD023"),
300 ("MD024", "MD024"),
301 ("MD025", "MD025"),
302 ("MD026", "MD026"),
303 ("MD027", "MD027"),
304 ("MD028", "MD028"),
305 ("MD029", "MD029"),
306 ("MD030", "MD030"),
307 ("MD031", "MD031"),
308 ("MD032", "MD032"),
309 ("MD033", "MD033"),
310 ("MD034", "MD034"),
311 ("MD035", "MD035"),
312 ("MD036", "MD036"),
313 ("MD037", "MD037"),
314 ("MD038", "MD038"),
315 ("MD039", "MD039"),
316 ("MD040", "MD040"),
317 ("MD041", "MD041"),
318 ("MD042", "MD042"),
319 ("MD043", "MD043"),
320 ("MD044", "MD044"),
321 ("MD045", "MD045"),
322 ("MD046", "MD046"),
323 ("MD047", "MD047"),
324 ("MD048", "MD048"),
325 ("MD049", "MD049"),
326 ("MD050", "MD050"),
327 ("MD051", "MD051"),
328 ("MD052", "MD052"),
329 ("MD053", "MD053"),
330 ("MD054", "MD054"),
331 ("MD055", "MD055"),
332 ("MD056", "MD056"),
333 ("MD057", "MD057"),
334 ("MD058", "MD058"),
335 ("MD059", "MD059"),
336 ("MD060", "MD060"),
337 ("MD061", "MD061"),
338 ("MD062", "MD062"),
339 ("MD063", "MD063"),
340 ("MD064", "MD064"),
341 ("MD065", "MD065"),
342 ("MD066", "MD066"),
343 ("MD067", "MD067"),
344 ("MD068", "MD068"),
345 ("MD069", "MD069"),
346 ("MD070", "MD070"),
347 ("MD071", "MD071"),
348 ("MD072", "MD072"),
349 ("MD073", "MD073"),
350 ("MD074", "MD074"),
351 ("MD075", "MD075"),
352 ("MD076", "MD076"),
353 ("MD077", "MD077"),
354 ("MD078", "MD078"),
355 ("MD079", "MD079"),
356 ("MD080", "MD080"),
357 ("MD081", "MD081"),
358 ("MD082", "MD082"),
359 ("MD083", "MD083"),
360 ("MD084", "MD084"),
361 ("MD085", "MD085"),
362 ("MD086", "MD086"),
363 ("MD087", "MD087"),
364 ("MD088", "MD088"),
365 ("MISSING-CHUNK-LABELS", "MD078"),
366 ("MKDOCS-NAV", "MD074"),
367 ("MOJIBAKE", "MD083"),
368 ("NESTED-CODE-FENCE", "MD070"),
369 ("NO-ALT-TEXT", "MD045"),
370 ("NO-BARE-URLS", "MD034"),
371 ("NO-BLANKS-BLOCKQUOTE", "MD028"),
372 ("NO-DUPLICATE-HEADING", "MD024"),
373 ("NO-DUPLICATE-LIST-MARKERS", "MD069"),
374 ("NO-EMPHASIS-AS-HEADING", "MD036"),
375 ("NO-EMPTY-LINKS", "MD042"),
376 ("NO-EMPTY-SECTIONS", "MD082"),
377 ("NO-EXCESSIVE-EMPHASIS", "MD081"),
378 ("NO-HARD-TABS", "MD010"),
379 ("NO-INLINE-HTML", "MD033"),
380 ("NO-MISSING-SPACE-ATX", "MD018"),
381 ("NO-MISSING-SPACE-CLOSED-ATX", "MD020"),
382 ("NO-MULTIPLE-BLANKS", "MD012"),
383 ("NO-MULTIPLE-CONSECUTIVE-SPACES", "MD064"),
384 ("NO-MULTIPLE-SPACE-ATX", "MD019"),
385 ("NO-MULTIPLE-SPACE-BLOCKQUOTE", "MD027"),
386 ("NO-MULTIPLE-SPACE-CLOSED-ATX", "MD021"),
387 ("NO-REVERSED-LINKS", "MD011"),
388 ("NO-SPACE-IN-CODE", "MD038"),
389 ("NO-SPACE-IN-EMPHASIS", "MD037"),
390 ("NO-SPACE-IN-LINK-DESTINATION", "MD062"),
391 ("NO-SPACE-IN-LINKS", "MD039"),
392 ("NO-TRAILING-PUNCTUATION", "MD026"),
393 ("NO-TRAILING-SPACES", "MD009"),
394 ("NO-UNCLOSED-COMMENTS", "MD086"),
395 ("OL-PREFIX", "MD029"),
396 ("ORPHANED-TABLE-ROWS", "MD075"),
397 ("PARAGRAPH-CONTINUATION-INDENT", "MD085"),
398 ("PROPER-NAMES", "MD044"),
399 ("QUOTES-DASHES", "MD088"),
400 ("REFERENCE-LINKS-IMAGES", "MD052"),
401 ("REQUIRED-HEADINGS", "MD043"),
402 ("SINGLE-H1", "MD025"),
403 ("SINGLE-TITLE", "MD025"),
404 ("SINGLE-TRAILING-NEWLINE", "MD047"),
405 ("STRONG-STYLE", "MD050"),
406 ("TABLE-CELL-ALIGNMENT", "MD060"),
407 ("TABLE-COLUMN-COUNT", "MD056"),
408 ("TABLE-FORMAT", "MD060"),
409 ("TABLE-PIPE-STYLE", "MD055"),
410 ("TOC-VALIDATION", "MD073"),
411 ("UL-INDENT", "MD007"),
412 ("UL-STYLE", "MD004"),
413 ("UNUSED-DISABLE-COMMENT", "MD087"),
414]);
415
416pub static RULE_PRIMARY_ALIAS: StaticMap = StaticMap::new(&[
425 ("MD001", "heading-increment"),
426 ("MD003", "heading-style"),
427 ("MD004", "ul-style"),
428 ("MD005", "list-indent"),
429 ("MD007", "ul-indent"),
430 ("MD009", "no-trailing-spaces"),
431 ("MD010", "no-hard-tabs"),
432 ("MD011", "no-reversed-links"),
433 ("MD012", "no-multiple-blanks"),
434 ("MD013", "line-length"),
435 ("MD014", "commands-show-output"),
436 ("MD018", "no-missing-space-atx"),
437 ("MD019", "no-multiple-space-atx"),
438 ("MD020", "no-missing-space-closed-atx"),
439 ("MD021", "no-multiple-space-closed-atx"),
440 ("MD022", "blanks-around-headings"),
441 ("MD023", "heading-start-left"),
442 ("MD024", "no-duplicate-heading"),
443 ("MD025", "single-title"),
444 ("MD026", "no-trailing-punctuation"),
445 ("MD027", "no-multiple-space-blockquote"),
446 ("MD028", "no-blanks-blockquote"),
447 ("MD029", "ol-prefix"),
448 ("MD030", "list-marker-space"),
449 ("MD031", "blanks-around-fences"),
450 ("MD032", "blanks-around-lists"),
451 ("MD033", "no-inline-html"),
452 ("MD034", "no-bare-urls"),
453 ("MD035", "hr-style"),
454 ("MD036", "no-emphasis-as-heading"),
455 ("MD037", "no-space-in-emphasis"),
456 ("MD038", "no-space-in-code"),
457 ("MD039", "no-space-in-links"),
458 ("MD040", "fenced-code-language"),
459 ("MD041", "first-line-heading"),
460 ("MD042", "no-empty-links"),
461 ("MD043", "required-headings"),
462 ("MD044", "proper-names"),
463 ("MD045", "no-alt-text"),
464 ("MD046", "code-block-style"),
465 ("MD047", "single-trailing-newline"),
466 ("MD048", "code-fence-style"),
467 ("MD049", "emphasis-style"),
468 ("MD050", "strong-style"),
469 ("MD051", "link-fragments"),
470 ("MD052", "reference-links-images"),
471 ("MD053", "link-image-reference-definitions"),
472 ("MD054", "link-image-style"),
473 ("MD055", "table-pipe-style"),
474 ("MD056", "table-column-count"),
475 ("MD057", "existing-relative-links"),
476 ("MD058", "blanks-around-tables"),
477 ("MD059", "descriptive-link-text"),
478 ("MD060", "table-format"),
479 ("MD061", "forbidden-terms"),
480 ("MD062", "link-destination-whitespace"),
481 ("MD063", "heading-capitalization"),
482 ("MD064", "no-multiple-consecutive-spaces"),
483 ("MD065", "blanks-around-horizontal-rules"),
484 ("MD066", "footnote-validation"),
485 ("MD067", "footnote-definition-order"),
486 ("MD068", "empty-footnote-definition"),
487 ("MD069", "no-duplicate-list-markers"),
488 ("MD070", "nested-code-fence"),
489 ("MD071", "blank-line-after-frontmatter"),
490 ("MD072", "frontmatter-key-sort"),
491 ("MD073", "toc-validation"),
492 ("MD074", "mkdocs-nav"),
493 ("MD075", "orphaned-table-rows"),
494 ("MD076", "list-item-spacing"),
495 ("MD077", "list-continuation-indent"),
496 ("MD078", "missing-chunk-labels"),
497 ("MD079", "chunk-label-spaces"),
498 ("MD080", "heading-anchor-collision"),
499 ("MD081", "no-excessive-emphasis"),
500 ("MD082", "no-empty-sections"),
501 ("MD083", "mojibake"),
502 ("MD084", "invisible-characters"),
503 ("MD085", "paragraph-continuation-indent"),
504 ("MD086", "no-unclosed-comments"),
505 ("MD087", "unused-disable-comment"),
506 ("MD088", "quotes-dashes"),
507]);
508
509pub fn primary_alias(rule_id: &str) -> Option<&'static str> {
514 RULE_PRIMARY_ALIAS.get(rule_id)
515}
516
517pub fn resolve_rule_name_alias(key: &str) -> Option<&'static str> {
521 let normalized_key = key.to_ascii_uppercase().replace('_', "-");
523
524 RULE_ALIAS_MAP.get(normalized_key.as_str())
525}
526
527pub fn resolve_rule_name(name: &str) -> String {
535 resolve_rule_name_alias(name).map_or_else(|| normalize_key(name), std::string::ToString::to_string)
536}
537
538pub fn resolve_rule_names(input: &str) -> std::collections::HashSet<String> {
542 input
543 .split(',')
544 .map(str::trim)
545 .filter(|s| !s.is_empty())
546 .map(resolve_rule_name)
547 .collect()
548}
549
550pub fn is_valid_rule_name(name: &str) -> bool {
554 if name.eq_ignore_ascii_case("all") {
556 return true;
557 }
558 resolve_rule_name_alias(name).is_some()
559}
560
561pub fn canonicalize_rule_list_in_place(list: &mut Vec<String>) {
573 if list.is_empty() {
574 return;
575 }
576 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::with_capacity(list.len());
577 let mut out: Vec<String> = Vec::with_capacity(list.len());
578 for entry in list.drain(..) {
579 let canonical = if entry.eq_ignore_ascii_case("all") {
580 "all".to_string()
581 } else {
582 resolve_rule_name(&entry)
583 };
584 if seen.insert(canonical.clone()) {
585 out.push(canonical);
586 }
587 }
588 *list = out;
589}
590
591#[cfg(test)]
592mod primary_alias_tests {
593 use super::{RULE_ALIAS_MAP, RULE_PRIMARY_ALIAS, default_registry, primary_alias, resolve_rule_name_alias};
594
595 fn aliases_by_rule() -> std::collections::BTreeMap<&'static str, Vec<&'static str>> {
597 let mut by_rule: std::collections::BTreeMap<&'static str, Vec<&'static str>> =
598 std::collections::BTreeMap::new();
599 for (alias, rule_id) in RULE_ALIAS_MAP.entries() {
600 let entry = by_rule.entry(rule_id).or_default();
601 if alias != rule_id {
602 entry.push(alias);
603 }
604 }
605 by_rule
606 }
607
608 #[test]
612 fn the_rule_name_tables_are_sorted_by_key() {
613 assert!(RULE_ALIAS_MAP.is_sorted_by_key(), "RULE_ALIAS_MAP is out of order");
614 assert!(
615 RULE_PRIMARY_ALIAS.is_sorted_by_key(),
616 "RULE_PRIMARY_ALIAS is out of order"
617 );
618 }
619
620 #[test]
623 fn every_key_in_the_rule_name_tables_is_reachable() {
624 for (key, value) in RULE_ALIAS_MAP.entries() {
625 assert_eq!(RULE_ALIAS_MAP.get(key), Some(value), "RULE_ALIAS_MAP lost '{key}'");
626 }
627 for (key, value) in RULE_PRIMARY_ALIAS.entries() {
628 assert_eq!(
629 RULE_PRIMARY_ALIAS.get(key),
630 Some(value),
631 "RULE_PRIMARY_ALIAS lost '{key}'"
632 );
633 }
634 assert_eq!(
635 RULE_ALIAS_MAP.get("NOT-A-RULE"),
636 None,
637 "control: a name the table does not hold answers None"
638 );
639 }
640
641 #[test]
642 fn every_rule_has_a_readable_name() {
643 let rule_ids = default_registry().rule_names();
644 assert!(
645 rule_ids.contains("MD013"),
646 "control: the registry lists rules by canonical ID, got {rule_ids:?}"
647 );
648 let missing: Vec<_> = rule_ids
649 .into_iter()
650 .filter(|rule_id| primary_alias(rule_id).is_none())
651 .collect();
652 assert!(
653 missing.is_empty(),
654 "these rules have no entry in RULE_PRIMARY_ALIAS: {missing:?}"
655 );
656 }
657
658 #[test]
659 fn a_readable_name_is_one_of_the_rules_own_aliases() {
660 let by_rule = aliases_by_rule();
661 for (rule_id, primary) in RULE_PRIMARY_ALIAS.entries() {
662 let aliases = by_rule
663 .get(rule_id)
664 .unwrap_or_else(|| panic!("{rule_id} has a readable name but is not in RULE_ALIAS_MAP"));
665 assert!(
666 aliases.iter().any(|alias| alias.eq_ignore_ascii_case(primary)),
667 "{rule_id}'s readable name '{primary}' is not one of its aliases {aliases:?}"
668 );
669 }
670 }
671
672 #[test]
673 fn a_readable_name_resolves_back_to_its_rule() {
674 for (rule_id, primary) in RULE_PRIMARY_ALIAS.entries() {
675 assert_eq!(
676 resolve_rule_name_alias(primary),
677 Some(rule_id),
678 "'{primary}' must be usable anywhere a rule name is accepted"
679 );
680 }
681 }
682
683 #[test]
684 fn a_name_that_is_not_a_rule_id_has_no_readable_name() {
685 assert_eq!(primary_alias("MD013"), Some("line-length"));
688 assert_eq!(primary_alias("line-length"), None);
689 assert_eq!(primary_alias("MD999"), None);
690 }
691}
692
693#[cfg(test)]
694mod canonicalize_tests {
695 use super::canonicalize_rule_list_in_place;
696
697 #[test]
698 fn rewrites_aliases_to_canonical_ids() {
699 let mut list = vec!["no-inline-html".to_string(), "line-length".to_string()];
700 canonicalize_rule_list_in_place(&mut list);
701 assert_eq!(list, vec!["MD033".to_string(), "MD013".to_string()]);
702 }
703
704 #[test]
705 fn dedupes_alias_and_canonical_preserving_order() {
706 let mut list = vec!["MD033".to_string(), "no-inline-html".to_string(), "MD013".to_string()];
707 canonicalize_rule_list_in_place(&mut list);
708 assert_eq!(list, vec!["MD033".to_string(), "MD013".to_string()]);
709 }
710
711 #[test]
712 fn preserves_all_keyword_normalized() {
713 let mut list = vec!["ALL".to_string(), "MD013".to_string()];
714 canonicalize_rule_list_in_place(&mut list);
715 assert_eq!(list, vec!["all".to_string(), "MD013".to_string()]);
716 }
717
718 #[test]
719 fn is_idempotent() {
720 let mut list = vec!["no-inline-html".to_string(), "MD013".to_string()];
721 canonicalize_rule_list_in_place(&mut list);
722 let once = list.clone();
723 canonicalize_rule_list_in_place(&mut list);
724 assert_eq!(list, once);
725 }
726
727 #[test]
728 fn handles_empty_and_unknown_inputs() {
729 let mut empty: Vec<String> = Vec::new();
730 canonicalize_rule_list_in_place(&mut empty);
731 assert!(empty.is_empty());
732
733 let mut unknown = vec!["custom-rule".to_string(), "Custom-Rule".to_string()];
734 canonicalize_rule_list_in_place(&mut unknown);
735 assert_eq!(unknown, vec!["custom-rule".to_string()]);
737 }
738}