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.default_config_section() {
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 expected_value_for(&self, rule: &str, key: &str) -> Option<&toml::Value> {
123 let schema = self.rule_schemas.get(rule)?;
124
125 if let Some(aliases) = self.rule_aliases.get(rule)
127 && let Some(canonical_key) = aliases.get(key)
128 && let Some(value) = schema.get(canonical_key)
129 {
130 return filter_type_check_sentinels(value);
131 }
132
133 if let Some(value) = schema.get(key) {
135 return filter_type_check_sentinels(value);
136 }
137
138 let key_variants = [
140 key.replace('-', "_"), key.replace('_', "-"), normalize_key(key), ];
144
145 for variant in &key_variants {
146 if let Some(value) = schema.get(variant) {
147 return filter_type_check_sentinels(value);
148 }
149 }
150
151 None
152 }
153
154 pub fn resolve_rule_name(&self, name: &str) -> Option<String> {
161 let normalized = normalize_key(name);
163 if self.rule_schemas.contains_key(&normalized) {
164 return Some(normalized);
165 }
166
167 resolve_rule_name_alias(name).map(std::string::ToString::to_string)
169 }
170}
171
172fn filter_type_check_sentinels(value: &toml::Value) -> Option<&toml::Value> {
176 if crate::rule_config_serde::is_nullable_sentinel(value) || crate::rule_config_serde::is_polymorphic_sentinel(value)
177 {
178 None
179 } else {
180 Some(value)
181 }
182}
183
184pub static RULE_ALIAS_MAP: phf::Map<&'static str, &'static str> = phf::phf_map! {
187 "MD001" => "MD001",
189 "MD003" => "MD003",
190 "MD004" => "MD004",
191 "MD005" => "MD005",
192 "MD007" => "MD007",
193 "MD009" => "MD009",
194 "MD010" => "MD010",
195 "MD011" => "MD011",
196 "MD012" => "MD012",
197 "MD013" => "MD013",
198 "MD014" => "MD014",
199 "MD018" => "MD018",
200 "MD019" => "MD019",
201 "MD020" => "MD020",
202 "MD021" => "MD021",
203 "MD022" => "MD022",
204 "MD023" => "MD023",
205 "MD024" => "MD024",
206 "MD025" => "MD025",
207 "MD026" => "MD026",
208 "MD027" => "MD027",
209 "MD028" => "MD028",
210 "MD029" => "MD029",
211 "MD030" => "MD030",
212 "MD031" => "MD031",
213 "MD032" => "MD032",
214 "MD033" => "MD033",
215 "MD034" => "MD034",
216 "MD035" => "MD035",
217 "MD036" => "MD036",
218 "MD037" => "MD037",
219 "MD038" => "MD038",
220 "MD039" => "MD039",
221 "MD040" => "MD040",
222 "MD041" => "MD041",
223 "MD042" => "MD042",
224 "MD043" => "MD043",
225 "MD044" => "MD044",
226 "MD045" => "MD045",
227 "MD046" => "MD046",
228 "MD047" => "MD047",
229 "MD048" => "MD048",
230 "MD049" => "MD049",
231 "MD050" => "MD050",
232 "MD051" => "MD051",
233 "MD052" => "MD052",
234 "MD053" => "MD053",
235 "MD054" => "MD054",
236 "MD055" => "MD055",
237 "MD056" => "MD056",
238 "MD057" => "MD057",
239 "MD058" => "MD058",
240 "MD059" => "MD059",
241 "MD060" => "MD060",
242 "MD061" => "MD061",
243 "MD062" => "MD062",
244 "MD063" => "MD063",
245 "MD064" => "MD064",
246 "MD065" => "MD065",
247 "MD066" => "MD066",
248 "MD067" => "MD067",
249 "MD068" => "MD068",
250 "MD069" => "MD069",
251 "MD070" => "MD070",
252 "MD071" => "MD071",
253 "MD072" => "MD072",
254 "MD073" => "MD073",
255 "MD074" => "MD074",
256 "MD075" => "MD075",
257 "MD076" => "MD076",
258 "MD077" => "MD077",
259 "MD078" => "MD078",
260 "MD079" => "MD079",
261 "MD080" => "MD080",
262 "MD081" => "MD081",
263 "MD082" => "MD082",
264 "MD083" => "MD083",
265 "MD084" => "MD084",
266 "MD085" => "MD085",
267 "MD086" => "MD086",
268 "MD087" => "MD087",
269
270 "HEADING-INCREMENT" => "MD001",
272 "HEADING-STYLE" => "MD003",
273 "UL-STYLE" => "MD004",
274 "LIST-INDENT" => "MD005",
275 "UL-INDENT" => "MD007",
276 "NO-TRAILING-SPACES" => "MD009",
277 "NO-HARD-TABS" => "MD010",
278 "NO-REVERSED-LINKS" => "MD011",
279 "NO-MULTIPLE-BLANKS" => "MD012",
280 "LINE-LENGTH" => "MD013",
281 "COMMANDS-SHOW-OUTPUT" => "MD014",
282 "NO-MISSING-SPACE-ATX" => "MD018",
283 "NO-MULTIPLE-SPACE-ATX" => "MD019",
284 "NO-MISSING-SPACE-CLOSED-ATX" => "MD020",
285 "NO-MULTIPLE-SPACE-CLOSED-ATX" => "MD021",
286 "BLANKS-AROUND-HEADINGS" => "MD022",
287 "HEADING-START-LEFT" => "MD023",
288 "NO-DUPLICATE-HEADING" => "MD024",
289 "SINGLE-TITLE" => "MD025",
290 "SINGLE-H1" => "MD025",
291 "NO-TRAILING-PUNCTUATION" => "MD026",
292 "NO-MULTIPLE-SPACE-BLOCKQUOTE" => "MD027",
293 "NO-BLANKS-BLOCKQUOTE" => "MD028",
294 "OL-PREFIX" => "MD029",
295 "LIST-MARKER-SPACE" => "MD030",
296 "BLANKS-AROUND-FENCES" => "MD031",
297 "BLANKS-AROUND-LISTS" => "MD032",
298 "NO-INLINE-HTML" => "MD033",
299 "NO-BARE-URLS" => "MD034",
300 "HR-STYLE" => "MD035",
301 "NO-EMPHASIS-AS-HEADING" => "MD036",
302 "NO-SPACE-IN-EMPHASIS" => "MD037",
303 "NO-SPACE-IN-CODE" => "MD038",
304 "NO-SPACE-IN-LINKS" => "MD039",
305 "FENCED-CODE-LANGUAGE" => "MD040",
306 "FIRST-LINE-HEADING" => "MD041",
307 "FIRST-LINE-H1" => "MD041",
308 "NO-EMPTY-LINKS" => "MD042",
309 "REQUIRED-HEADINGS" => "MD043",
310 "PROPER-NAMES" => "MD044",
311 "NO-ALT-TEXT" => "MD045",
312 "CODE-BLOCK-STYLE" => "MD046",
313 "SINGLE-TRAILING-NEWLINE" => "MD047",
314 "CODE-FENCE-STYLE" => "MD048",
315 "EMPHASIS-STYLE" => "MD049",
316 "STRONG-STYLE" => "MD050",
317 "LINK-FRAGMENTS" => "MD051",
318 "REFERENCE-LINKS-IMAGES" => "MD052",
319 "LINK-IMAGE-REFERENCE-DEFINITIONS" => "MD053",
320 "LINK-IMAGE-STYLE" => "MD054",
321 "TABLE-PIPE-STYLE" => "MD055",
322 "TABLE-COLUMN-COUNT" => "MD056",
323 "EXISTING-RELATIVE-LINKS" => "MD057",
324 "BLANKS-AROUND-TABLES" => "MD058",
325 "DESCRIPTIVE-LINK-TEXT" => "MD059",
326 "TABLE-CELL-ALIGNMENT" => "MD060",
327 "TABLE-FORMAT" => "MD060",
328 "FORBIDDEN-TERMS" => "MD061",
329 "LINK-DESTINATION-WHITESPACE" => "MD062",
330 "NO-SPACE-IN-LINK-DESTINATION" => "MD062",
331 "HEADING-CAPITALIZATION" => "MD063",
332 "NO-MULTIPLE-CONSECUTIVE-SPACES" => "MD064",
333 "BLANKS-AROUND-HORIZONTAL-RULES" => "MD065",
334 "FOOTNOTE-VALIDATION" => "MD066",
335 "FOOTNOTE-DEFINITION-ORDER" => "MD067",
336 "EMPTY-FOOTNOTE-DEFINITION" => "MD068",
337 "NO-DUPLICATE-LIST-MARKERS" => "MD069",
338 "NESTED-CODE-FENCE" => "MD070",
339 "BLANK-LINE-AFTER-FRONTMATTER" => "MD071",
340 "FRONTMATTER-KEY-SORT" => "MD072",
341 "TOC-VALIDATION" => "MD073",
342 "MKDOCS-NAV" => "MD074",
343 "ORPHANED-TABLE-ROWS" => "MD075",
344 "LIST-ITEM-SPACING" => "MD076",
345 "LIST-CONTINUATION-INDENT" => "MD077",
346 "MISSING-CHUNK-LABELS" => "MD078",
347 "CHUNK-LABEL-SPACES" => "MD079",
348 "HEADING-ANCHOR-COLLISION" => "MD080",
349 "NO-EXCESSIVE-EMPHASIS" => "MD081",
350 "NO-EMPTY-SECTIONS" => "MD082",
351 "MOJIBAKE" => "MD083",
352 "INVISIBLE-CHARACTERS" => "MD084",
353 "PARAGRAPH-CONTINUATION-INDENT" => "MD085",
354 "NO-UNCLOSED-COMMENTS" => "MD086",
355 "UNUSED-DISABLE-COMMENT" => "MD087",
356};
357
358pub static RULE_PRIMARY_ALIAS: phf::Map<&'static str, &'static str> = phf::phf_map! {
365 "MD001" => "heading-increment",
366 "MD003" => "heading-style",
367 "MD004" => "ul-style",
368 "MD005" => "list-indent",
369 "MD007" => "ul-indent",
370 "MD009" => "no-trailing-spaces",
371 "MD010" => "no-hard-tabs",
372 "MD011" => "no-reversed-links",
373 "MD012" => "no-multiple-blanks",
374 "MD013" => "line-length",
375 "MD014" => "commands-show-output",
376 "MD018" => "no-missing-space-atx",
377 "MD019" => "no-multiple-space-atx",
378 "MD020" => "no-missing-space-closed-atx",
379 "MD021" => "no-multiple-space-closed-atx",
380 "MD022" => "blanks-around-headings",
381 "MD023" => "heading-start-left",
382 "MD024" => "no-duplicate-heading",
383 "MD025" => "single-title",
384 "MD026" => "no-trailing-punctuation",
385 "MD027" => "no-multiple-space-blockquote",
386 "MD028" => "no-blanks-blockquote",
387 "MD029" => "ol-prefix",
388 "MD030" => "list-marker-space",
389 "MD031" => "blanks-around-fences",
390 "MD032" => "blanks-around-lists",
391 "MD033" => "no-inline-html",
392 "MD034" => "no-bare-urls",
393 "MD035" => "hr-style",
394 "MD036" => "no-emphasis-as-heading",
395 "MD037" => "no-space-in-emphasis",
396 "MD038" => "no-space-in-code",
397 "MD039" => "no-space-in-links",
398 "MD040" => "fenced-code-language",
399 "MD041" => "first-line-heading",
400 "MD042" => "no-empty-links",
401 "MD043" => "required-headings",
402 "MD044" => "proper-names",
403 "MD045" => "no-alt-text",
404 "MD046" => "code-block-style",
405 "MD047" => "single-trailing-newline",
406 "MD048" => "code-fence-style",
407 "MD049" => "emphasis-style",
408 "MD050" => "strong-style",
409 "MD051" => "link-fragments",
410 "MD052" => "reference-links-images",
411 "MD053" => "link-image-reference-definitions",
412 "MD054" => "link-image-style",
413 "MD055" => "table-pipe-style",
414 "MD056" => "table-column-count",
415 "MD057" => "existing-relative-links",
416 "MD058" => "blanks-around-tables",
417 "MD059" => "descriptive-link-text",
418 "MD060" => "table-format",
419 "MD061" => "forbidden-terms",
420 "MD062" => "link-destination-whitespace",
421 "MD063" => "heading-capitalization",
422 "MD064" => "no-multiple-consecutive-spaces",
423 "MD065" => "blanks-around-horizontal-rules",
424 "MD066" => "footnote-validation",
425 "MD067" => "footnote-definition-order",
426 "MD068" => "empty-footnote-definition",
427 "MD069" => "no-duplicate-list-markers",
428 "MD070" => "nested-code-fence",
429 "MD071" => "blank-line-after-frontmatter",
430 "MD072" => "frontmatter-key-sort",
431 "MD073" => "toc-validation",
432 "MD074" => "mkdocs-nav",
433 "MD075" => "orphaned-table-rows",
434 "MD076" => "list-item-spacing",
435 "MD077" => "list-continuation-indent",
436 "MD078" => "missing-chunk-labels",
437 "MD079" => "chunk-label-spaces",
438 "MD080" => "heading-anchor-collision",
439 "MD081" => "no-excessive-emphasis",
440 "MD082" => "no-empty-sections",
441 "MD083" => "mojibake",
442 "MD084" => "invisible-characters",
443 "MD085" => "paragraph-continuation-indent",
444 "MD086" => "no-unclosed-comments",
445 "MD087" => "unused-disable-comment",
446};
447
448pub fn primary_alias(rule_id: &str) -> Option<&'static str> {
453 RULE_PRIMARY_ALIAS.get(rule_id).copied()
454}
455
456pub fn resolve_rule_name_alias(key: &str) -> Option<&'static str> {
460 let normalized_key = key.to_ascii_uppercase().replace('_', "-");
462
463 RULE_ALIAS_MAP.get(normalized_key.as_str()).copied()
465}
466
467pub fn resolve_rule_name(name: &str) -> String {
475 resolve_rule_name_alias(name).map_or_else(|| normalize_key(name), std::string::ToString::to_string)
476}
477
478pub fn resolve_rule_names(input: &str) -> std::collections::HashSet<String> {
482 input
483 .split(',')
484 .map(str::trim)
485 .filter(|s| !s.is_empty())
486 .map(resolve_rule_name)
487 .collect()
488}
489
490pub fn is_valid_rule_name(name: &str) -> bool {
494 if name.eq_ignore_ascii_case("all") {
496 return true;
497 }
498 resolve_rule_name_alias(name).is_some()
499}
500
501pub fn canonicalize_rule_list_in_place(list: &mut Vec<String>) {
513 if list.is_empty() {
514 return;
515 }
516 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::with_capacity(list.len());
517 let mut out: Vec<String> = Vec::with_capacity(list.len());
518 for entry in list.drain(..) {
519 let canonical = if entry.eq_ignore_ascii_case("all") {
520 "all".to_string()
521 } else {
522 resolve_rule_name(&entry)
523 };
524 if seen.insert(canonical.clone()) {
525 out.push(canonical);
526 }
527 }
528 *list = out;
529}
530
531#[cfg(test)]
532mod primary_alias_tests {
533 use super::{RULE_ALIAS_MAP, RULE_PRIMARY_ALIAS, default_registry, primary_alias, resolve_rule_name_alias};
534
535 fn aliases_by_rule() -> std::collections::BTreeMap<&'static str, Vec<&'static str>> {
537 let mut by_rule: std::collections::BTreeMap<&'static str, Vec<&'static str>> =
538 std::collections::BTreeMap::new();
539 for (alias, rule_id) in RULE_ALIAS_MAP.entries() {
540 let entry = by_rule.entry(*rule_id).or_default();
541 if alias != rule_id {
542 entry.push(*alias);
543 }
544 }
545 by_rule
546 }
547
548 #[test]
549 fn every_rule_has_a_readable_name() {
550 let rule_ids = default_registry().rule_names();
551 assert!(
552 rule_ids.contains("MD013"),
553 "control: the registry lists rules by canonical ID, got {rule_ids:?}"
554 );
555 let missing: Vec<_> = rule_ids
556 .into_iter()
557 .filter(|rule_id| primary_alias(rule_id).is_none())
558 .collect();
559 assert!(
560 missing.is_empty(),
561 "these rules have no entry in RULE_PRIMARY_ALIAS: {missing:?}"
562 );
563 }
564
565 #[test]
566 fn a_readable_name_is_one_of_the_rules_own_aliases() {
567 let by_rule = aliases_by_rule();
568 for (rule_id, primary) in RULE_PRIMARY_ALIAS.entries() {
569 let aliases = by_rule
570 .get(rule_id)
571 .unwrap_or_else(|| panic!("{rule_id} has a readable name but is not in RULE_ALIAS_MAP"));
572 assert!(
573 aliases.iter().any(|alias| alias.eq_ignore_ascii_case(primary)),
574 "{rule_id}'s readable name '{primary}' is not one of its aliases {aliases:?}"
575 );
576 }
577 }
578
579 #[test]
580 fn a_readable_name_resolves_back_to_its_rule() {
581 for (rule_id, primary) in RULE_PRIMARY_ALIAS.entries() {
582 assert_eq!(
583 resolve_rule_name_alias(primary),
584 Some(*rule_id),
585 "'{primary}' must be usable anywhere a rule name is accepted"
586 );
587 }
588 }
589
590 #[test]
591 fn a_name_that_is_not_a_rule_id_has_no_readable_name() {
592 assert_eq!(primary_alias("MD013"), Some("line-length"));
595 assert_eq!(primary_alias("line-length"), None);
596 assert_eq!(primary_alias("MD999"), None);
597 }
598}
599
600#[cfg(test)]
601mod canonicalize_tests {
602 use super::canonicalize_rule_list_in_place;
603
604 #[test]
605 fn rewrites_aliases_to_canonical_ids() {
606 let mut list = vec!["no-inline-html".to_string(), "line-length".to_string()];
607 canonicalize_rule_list_in_place(&mut list);
608 assert_eq!(list, vec!["MD033".to_string(), "MD013".to_string()]);
609 }
610
611 #[test]
612 fn dedupes_alias_and_canonical_preserving_order() {
613 let mut list = vec!["MD033".to_string(), "no-inline-html".to_string(), "MD013".to_string()];
614 canonicalize_rule_list_in_place(&mut list);
615 assert_eq!(list, vec!["MD033".to_string(), "MD013".to_string()]);
616 }
617
618 #[test]
619 fn preserves_all_keyword_normalized() {
620 let mut list = vec!["ALL".to_string(), "MD013".to_string()];
621 canonicalize_rule_list_in_place(&mut list);
622 assert_eq!(list, vec!["all".to_string(), "MD013".to_string()]);
623 }
624
625 #[test]
626 fn is_idempotent() {
627 let mut list = vec!["no-inline-html".to_string(), "MD013".to_string()];
628 canonicalize_rule_list_in_place(&mut list);
629 let once = list.clone();
630 canonicalize_rule_list_in_place(&mut list);
631 assert_eq!(list, once);
632 }
633
634 #[test]
635 fn handles_empty_and_unknown_inputs() {
636 let mut empty: Vec<String> = Vec::new();
637 canonicalize_rule_list_in_place(&mut empty);
638 assert!(empty.is_empty());
639
640 let mut unknown = vec!["custom-rule".to_string(), "Custom-Rule".to_string()];
641 canonicalize_rule_list_in_place(&mut unknown);
642 assert_eq!(unknown, vec!["custom-rule".to_string()]);
644 }
645}