1use serde::Serialize;
6use serde::de::DeserializeOwned;
7use std::sync::atomic::{AtomicBool, Ordering};
8
9pub trait RuleConfig: Serialize + DeserializeOwned + Default + Clone {
11 const RULE_NAME: &'static str;
13}
14
15pub fn load_rule_config<T: RuleConfig>(config: &crate::config::Config) -> T {
20 config
21 .rules
22 .get(T::RULE_NAME)
23 .and_then(|rule_config| {
24 let mut table = toml::map::Map::new();
26
27 for (k, v) in &rule_config.values {
28 table.insert(k.clone(), v.clone());
30 }
31
32 let toml_table = toml::Value::Table(table);
33
34 match toml_table.try_into::<T>() {
36 Ok(config) => Some(config),
37 Err(e) => {
38 let detail: &dyn std::fmt::Display = if config.withheld_rule_values.contains(T::RULE_NAME) {
44 &crate::config::WITHHELD
45 } else {
46 &e
47 };
48 eprintln!("Warning: Invalid configuration for rule {}: {detail}", T::RULE_NAME);
49 eprintln!("Using default values for rule {}.", T::RULE_NAME);
50 eprintln!("Hint: Check the documentation for valid configuration values.");
51
52 None
53 }
54 }
55 })
56 .unwrap_or_default()
57}
58
59pub fn compile_config_regex(pattern: &str, rule: &str, option: &str, values_withheld: bool) -> Option<regex::Regex> {
71 match regex::Regex::new(pattern) {
72 Ok(regex) => Some(regex),
73 Err(err) => {
74 let detail: &dyn std::fmt::Display = if values_withheld {
75 &crate::config::WITHHELD
76 } else {
77 &err
78 };
79 log::warn!("Invalid {option} for {rule}: {detail}. The option is ignored.");
80 None
81 }
82 }
83}
84
85pub fn option_is_explicit(config: &crate::config::Config, rule: &str, option: &str) -> bool {
92 config
93 .rules
94 .get(rule)
95 .is_some_and(|rule_config| rule_config.values.contains_key(option))
96}
97
98pub struct FlavorOverrideNotice(AtomicBool);
116
117impl FlavorOverrideNotice {
118 pub const fn new() -> Self {
119 Self(AtomicBool::new(false))
120 }
121
122 pub fn report(&self, rule: &str, option: &str, configured: &str, enforced: &str, reason: &str) {
126 if self.0.swap(true, Ordering::Relaxed) {
127 return;
128 }
129 eprintln!("{}", Self::message(rule, option, configured, enforced, reason));
130 }
131
132 fn message(rule: &str, option: &str, configured: &str, enforced: &str, reason: &str) -> String {
139 format!(
140 "\x1b[33m[config warning]\x1b[0m {rule}: {flavor} flavor requires {option}=\"{enforced}\" \
141 ({reason}). Overriding {option}=\"{configured}\" to {option}=\"{enforced}\".",
142 flavor = crate::config::MarkdownFlavor::MDG.name()
143 )
144 }
145}
146
147impl Default for FlavorOverrideNotice {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153const NULLABLE_SENTINEL: &str = "\0__nullable__";
158
159const POLYMORPHIC_SENTINEL: &str = "\0__polymorphic__";
166
167pub fn is_nullable_sentinel(value: &toml::Value) -> bool {
169 matches!(value, toml::Value::String(s) if s == NULLABLE_SENTINEL)
170}
171
172pub fn is_polymorphic_sentinel(value: &toml::Value) -> bool {
174 matches!(value, toml::Value::String(s) if s == POLYMORPHIC_SENTINEL)
175}
176
177pub fn polymorphic_sentinel_value() -> toml::Value {
183 toml::Value::String(POLYMORPHIC_SENTINEL.to_string())
184}
185
186pub fn config_schema_table<T: RuleConfig>(config: &T) -> Option<toml::map::Map<String, toml::Value>> {
192 let json_value = serde_json::to_value(config).ok()?;
193 let obj = json_value.as_object()?;
194 let mut table = toml::map::Map::new();
195 for (k, v) in obj {
196 if v.is_null() {
197 table.insert(k.clone(), toml::Value::String(NULLABLE_SENTINEL.to_string()));
198 } else {
199 let toml_v = json_to_toml_value(v).unwrap_or_else(|| toml::Value::String(NULLABLE_SENTINEL.to_string()));
202 table.insert(k.clone(), toml_v);
203 }
204 }
205 Some(table)
206}
207
208pub fn default_config_section_for<T: RuleConfig>() -> Option<(String, toml::Value)> {
216 let json_value = serde_json::to_value(T::default()).ok()?;
217 let toml_value = json_to_toml_value(&json_value)?;
218 match toml_value {
219 toml::Value::Table(table) if !table.is_empty() => Some((T::RULE_NAME.to_string(), toml::Value::Table(table))),
220 _ => None,
221 }
222}
223
224pub fn config_schema_for<T: RuleConfig>() -> Option<(String, toml::Value)> {
229 let table = config_schema_table(&T::default())?;
230 if table.is_empty() {
231 return None;
232 }
233 Some((T::RULE_NAME.to_string(), toml::Value::Table(table)))
234}
235
236#[macro_export]
244macro_rules! impl_rule_config_schema {
245 ($config_ty:ty) => {
246 fn config_schema(&self) -> Option<(String, toml::Value)> {
247 $crate::rule_config_serde::config_schema_for::<$config_ty>()
248 }
249 };
250}
251
252#[macro_export]
256macro_rules! impl_rule_config_sections {
257 ($config_ty:ty) => {
258 fn default_config_section(&self) -> Option<(String, toml::Value)> {
259 $crate::rule_config_serde::default_config_section_for::<$config_ty>()
260 }
261
262 $crate::impl_rule_config_schema!($config_ty);
263 };
264}
265
266#[macro_export]
270macro_rules! impl_rule_config_methods {
271 ($config_ty:ty) => {
272 $crate::impl_rule_config_sections!($config_ty);
273
274 fn from_config(config: &$crate::config::Config) -> Box<dyn $crate::rule::Rule>
275 where
276 Self: Sized,
277 {
278 Box::new(Self::from_config_struct(
279 $crate::rule_config_serde::load_rule_config::<$config_ty>(config),
280 ))
281 }
282 };
283}
284
285pub fn json_to_toml_value(json_val: &serde_json::Value) -> Option<toml::Value> {
287 match json_val {
288 serde_json::Value::Null => None,
289 serde_json::Value::Bool(b) => Some(toml::Value::Boolean(*b)),
290 serde_json::Value::Number(n) => {
291 if let Some(i) = n.as_i64() {
292 Some(toml::Value::Integer(i))
293 } else {
294 n.as_f64().map(toml::Value::Float)
295 }
296 }
297 serde_json::Value::String(s) => Some(toml::Value::String(s.clone())),
298 serde_json::Value::Array(arr) => {
299 let toml_arr: Vec<_> = arr.iter().filter_map(json_to_toml_value).collect();
300 Some(toml::Value::Array(toml_arr))
301 }
302 serde_json::Value::Object(obj) => {
303 let mut toml_table = toml::map::Map::new();
304 for (k, v) in obj {
305 if let Some(toml_v) = json_to_toml_value(v) {
306 toml_table.insert(k.clone(), toml_v);
307 }
308 }
309 Some(toml::Value::Table(toml_table))
310 }
311 }
312}
313
314pub fn is_rule_name(name: &str) -> bool {
318 let upper = name.to_ascii_uppercase();
319 upper.starts_with("MD") && upper.len() >= 4 && upper[2..].chars().all(|c| c.is_ascii_digit())
320}
321
322#[derive(Debug, Default)]
324pub struct RuleConfigConversion {
325 pub config: Option<crate::config::RuleConfig>,
327 pub warnings: Vec<String>,
329}
330
331pub fn json_to_rule_config(json_value: &serde_json::Value) -> Option<crate::config::RuleConfig> {
339 json_to_rule_config_with_warnings(json_value).config
340}
341
342pub fn json_to_rule_config_with_warnings(json_value: &serde_json::Value) -> RuleConfigConversion {
347 use std::collections::BTreeMap;
348
349 let mut result = RuleConfigConversion::default();
350
351 let Some(obj) = json_value.as_object() else {
352 result.warnings.push(format!(
353 "Expected object for rule config, got {}",
354 json_type_name(json_value)
355 ));
356 return result;
357 };
358
359 let mut values = BTreeMap::new();
360 let mut severity = None;
361
362 for (key, val) in obj {
363 if key == "severity" {
365 if let Some(s) = val.as_str() {
366 match s.to_lowercase().as_str() {
367 "error" => severity = Some(crate::rule::Severity::Error),
368 "warning" => severity = Some(crate::rule::Severity::Warning),
369 "info" => severity = Some(crate::rule::Severity::Info),
370 _ => {
371 result.warnings.push(format!(
372 "Invalid severity '{s}', expected 'error', 'warning', or 'info'"
373 ));
374 }
375 }
376 } else {
377 result
378 .warnings
379 .push(format!("Severity must be a string, got {}", json_type_name(val)));
380 }
381 continue;
382 }
383
384 if let Some(toml_val) = json_to_toml_value(val) {
386 values.insert(key.clone(), toml_val);
387 } else if !val.is_null() {
388 result
389 .warnings
390 .push(format!("Could not convert '{key}' value to config format"));
391 }
392 }
393
394 result.config = Some(crate::config::RuleConfig { severity, values });
395 result
396}
397
398fn json_type_name(val: &serde_json::Value) -> &'static str {
400 match val {
401 serde_json::Value::Null => "null",
402 serde_json::Value::Bool(_) => "boolean",
403 serde_json::Value::Number(_) => "number",
404 serde_json::Value::String(_) => "string",
405 serde_json::Value::Array(_) => "array",
406 serde_json::Value::Object(_) => "object",
407 }
408}
409
410pub fn toml_value_to_json(toml_val: &toml::Value) -> Option<serde_json::Value> {
412 match toml_val {
413 toml::Value::String(s) => Some(serde_json::Value::String(s.clone())),
414 toml::Value::Integer(i) => Some(serde_json::json!(i)),
415 toml::Value::Float(f) => Some(serde_json::json!(f)),
416 toml::Value::Boolean(b) => Some(serde_json::Value::Bool(*b)),
417 toml::Value::Array(arr) => {
418 let json_arr: Vec<_> = arr.iter().filter_map(toml_value_to_json).collect();
419 Some(serde_json::Value::Array(json_arr))
420 }
421 toml::Value::Table(table) => {
422 let mut json_obj = serde_json::Map::new();
423 for (k, v) in table {
424 if let Some(json_v) = toml_value_to_json(v) {
425 json_obj.insert(k.clone(), json_v);
426 }
427 }
428 Some(serde_json::Value::Object(json_obj))
429 }
430 toml::Value::Datetime(_) => None, }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use serde::{Deserialize, Serialize};
438 use std::collections::BTreeMap;
439
440 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
442 #[serde(default)]
443 struct TestRuleConfig {
444 #[serde(default)]
445 enabled: bool,
446 #[serde(default)]
447 indent: i64,
448 #[serde(default)]
449 style: String,
450 #[serde(default)]
451 items: Vec<String>,
452 }
453
454 impl RuleConfig for TestRuleConfig {
455 const RULE_NAME: &'static str = "TEST001";
456 }
457
458 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
462 #[serde(default)]
463 struct NullableTestConfig {
464 #[serde(default)]
465 enabled: bool,
466 #[serde(default, alias = "key-order")]
467 key_order: Option<Vec<String>>,
468 #[serde(default, alias = "title-pattern")]
469 title_pattern: Option<String>,
470 }
471
472 impl RuleConfig for NullableTestConfig {
473 const RULE_NAME: &'static str = "TEST_NULLABLE";
474 }
475
476 #[test]
477 fn test_is_nullable_sentinel() {
478 let sentinel = toml::Value::String(NULLABLE_SENTINEL.to_string());
479 assert!(is_nullable_sentinel(&sentinel));
480
481 let regular = toml::Value::String("normal".to_string());
482 assert!(!is_nullable_sentinel(®ular));
483
484 let integer = toml::Value::Integer(42);
485 assert!(!is_nullable_sentinel(&integer));
486 }
487
488 #[test]
489 fn test_is_polymorphic_sentinel() {
490 let sentinel = polymorphic_sentinel_value();
491 assert!(is_polymorphic_sentinel(&sentinel));
492
493 let nullable = toml::Value::String(NULLABLE_SENTINEL.to_string());
495 assert!(!is_polymorphic_sentinel(&nullable));
496 assert!(!is_nullable_sentinel(&sentinel));
497
498 let regular = toml::Value::String("normal".to_string());
499 assert!(!is_polymorphic_sentinel(®ular));
500 }
501
502 #[test]
503 fn test_config_schema_table_preserves_nullable_keys() {
504 let config = NullableTestConfig::default();
505 let table = config_schema_table(&config).unwrap();
506
507 assert!(table.contains_key("enabled"), "enabled key missing");
509 assert!(table.contains_key("key_order"), "key_order key missing");
510 assert!(table.contains_key("title_pattern"), "title_pattern key missing");
511
512 assert!(is_nullable_sentinel(table.get("key_order").unwrap()));
514 assert!(is_nullable_sentinel(table.get("title_pattern").unwrap()));
515
516 assert_eq!(table.get("enabled"), Some(&toml::Value::Boolean(false)));
518 }
519
520 #[test]
521 fn test_config_schema_table_non_null_option_uses_real_value() {
522 let config = NullableTestConfig {
523 enabled: true,
524 key_order: Some(vec!["title".to_string(), "date".to_string()]),
525 title_pattern: Some("pattern".to_string()),
526 };
527 let table = config_schema_table(&config).unwrap();
528
529 let key_order = table.get("key_order").unwrap();
531 assert!(!is_nullable_sentinel(key_order));
532 assert!(matches!(key_order, toml::Value::Array(_)));
533
534 let title_pattern = table.get("title_pattern").unwrap();
535 assert!(!is_nullable_sentinel(title_pattern));
536 assert_eq!(title_pattern, &toml::Value::String("pattern".to_string()));
537 }
538
539 #[test]
540 fn test_json_to_toml_value_still_drops_null() {
541 assert!(json_to_toml_value(&serde_json::Value::Null).is_none());
543 }
544
545 #[test]
546 fn test_config_schema_table_all_keys_present() {
547 let config = NullableTestConfig::default();
548 let table = config_schema_table(&config).unwrap();
549 assert_eq!(table.len(), 3, "Expected 3 keys: enabled, key_order, title_pattern");
550 }
551
552 #[test]
553 fn test_config_schema_table_never_drops_keys() {
554 let mut obj = serde_json::Map::new();
558 obj.insert("real_key".to_string(), serde_json::json!(42));
559 obj.insert("null_key".to_string(), serde_json::Value::Null);
560 let json = serde_json::Value::Object(obj);
561
562 let obj = json.as_object().unwrap();
564 let mut table = toml::map::Map::new();
565 for (k, v) in obj {
566 if v.is_null() {
567 table.insert(k.clone(), toml::Value::String(NULLABLE_SENTINEL.to_string()));
568 } else {
569 let toml_v =
570 json_to_toml_value(v).unwrap_or_else(|| toml::Value::String(NULLABLE_SENTINEL.to_string()));
571 table.insert(k.clone(), toml_v);
572 }
573 }
574
575 assert_eq!(table.len(), 2, "Both keys must be present");
576 assert!(table.contains_key("real_key"));
577 assert!(table.contains_key("null_key"));
578 }
579
580 #[test]
581 fn test_toml_value_to_json_basic_types() {
582 let toml_str = toml::Value::String("hello".to_string());
584 let json_str = toml_value_to_json(&toml_str).unwrap();
585 assert_eq!(json_str, serde_json::Value::String("hello".to_string()));
586
587 let toml_int = toml::Value::Integer(42);
589 let json_int = toml_value_to_json(&toml_int).unwrap();
590 assert_eq!(json_int, serde_json::json!(42));
591
592 let toml_float = toml::Value::Float(1.234);
594 let json_float = toml_value_to_json(&toml_float).unwrap();
595 assert_eq!(json_float, serde_json::json!(1.234));
596
597 let toml_bool = toml::Value::Boolean(true);
599 let json_bool = toml_value_to_json(&toml_bool).unwrap();
600 assert_eq!(json_bool, serde_json::Value::Bool(true));
601 }
602
603 #[test]
604 fn test_toml_value_to_json_complex_types() {
605 let toml_arr = toml::Value::Array(vec![
607 toml::Value::String("a".to_string()),
608 toml::Value::String("b".to_string()),
609 ]);
610 let json_arr = toml_value_to_json(&toml_arr).unwrap();
611 assert_eq!(json_arr, serde_json::json!(["a", "b"]));
612
613 let mut toml_table = toml::map::Map::new();
615 toml_table.insert("key1".to_string(), toml::Value::String("value1".to_string()));
616 toml_table.insert("key2".to_string(), toml::Value::Integer(123));
617 let toml_tbl = toml::Value::Table(toml_table);
618 let json_tbl = toml_value_to_json(&toml_tbl).unwrap();
619
620 let expected = serde_json::json!({
621 "key1": "value1",
622 "key2": 123
623 });
624 assert_eq!(json_tbl, expected);
625 }
626
627 #[test]
628 fn test_toml_value_to_json_datetime() {
629 let toml_dt = toml::Value::Datetime("2023-01-01T00:00:00Z".parse().unwrap());
631 assert!(toml_value_to_json(&toml_dt).is_none());
632 }
633
634 #[test]
635 fn test_json_to_toml_value_basic_types() {
636 assert!(json_to_toml_value(&serde_json::Value::Null).is_none());
638
639 let json_bool = serde_json::Value::Bool(false);
641 let toml_bool = json_to_toml_value(&json_bool).unwrap();
642 assert_eq!(toml_bool, toml::Value::Boolean(false));
643
644 let json_int = serde_json::json!(42);
646 let toml_int = json_to_toml_value(&json_int).unwrap();
647 assert_eq!(toml_int, toml::Value::Integer(42));
648
649 let json_float = serde_json::json!(1.234);
651 let toml_float = json_to_toml_value(&json_float).unwrap();
652 assert_eq!(toml_float, toml::Value::Float(1.234));
653
654 let json_str = serde_json::Value::String("test".to_string());
656 let toml_str = json_to_toml_value(&json_str).unwrap();
657 assert_eq!(toml_str, toml::Value::String("test".to_string()));
658 }
659
660 #[test]
661 fn test_json_to_toml_value_complex_types() {
662 let json_arr = serde_json::json!(["x", "y", "z"]);
664 let toml_arr = json_to_toml_value(&json_arr).unwrap();
665 if let toml::Value::Array(arr) = toml_arr {
666 assert_eq!(arr.len(), 3);
667 assert_eq!(arr[0], toml::Value::String("x".to_string()));
668 assert_eq!(arr[1], toml::Value::String("y".to_string()));
669 assert_eq!(arr[2], toml::Value::String("z".to_string()));
670 } else {
671 panic!("Expected array");
672 }
673
674 let json_obj = serde_json::json!({
676 "name": "test",
677 "count": 10,
678 "active": true
679 });
680 let toml_obj = json_to_toml_value(&json_obj).unwrap();
681 if let toml::Value::Table(table) = toml_obj {
682 assert_eq!(table.get("name"), Some(&toml::Value::String("test".to_string())));
683 assert_eq!(table.get("count"), Some(&toml::Value::Integer(10)));
684 assert_eq!(table.get("active"), Some(&toml::Value::Boolean(true)));
685 } else {
686 panic!("Expected table");
687 }
688 }
689
690 #[test]
691 fn test_load_rule_config_default() {
692 let config = crate::config::Config::default();
694
695 let rule_config: TestRuleConfig = load_rule_config(&config);
697 assert_eq!(rule_config, TestRuleConfig::default());
698 }
699
700 #[test]
701 fn test_load_rule_config_with_values() {
702 let mut config = crate::config::Config::default();
704 let mut rule_values = BTreeMap::new();
705 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
706 rule_values.insert("indent".to_string(), toml::Value::Integer(4));
707 rule_values.insert("style".to_string(), toml::Value::String("consistent".to_string()));
708 rule_values.insert(
709 "items".to_string(),
710 toml::Value::Array(vec![
711 toml::Value::String("item1".to_string()),
712 toml::Value::String("item2".to_string()),
713 ]),
714 );
715
716 config.rules.insert(
717 "TEST001".to_string(),
718 crate::config::RuleConfig {
719 severity: None,
720 values: rule_values,
721 },
722 );
723
724 let rule_config: TestRuleConfig = load_rule_config(&config);
726 assert!(rule_config.enabled);
727 assert_eq!(rule_config.indent, 4);
728 assert_eq!(rule_config.style, "consistent");
729 assert_eq!(rule_config.items, vec!["item1", "item2"]);
730 }
731
732 #[test]
733 fn test_load_rule_config_partial() {
734 let mut config = crate::config::Config::default();
736 let mut rule_values = BTreeMap::new();
737 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
738 rule_values.insert("style".to_string(), toml::Value::String("custom".to_string()));
739
740 config.rules.insert(
741 "TEST001".to_string(),
742 crate::config::RuleConfig {
743 severity: None,
744 values: rule_values,
745 },
746 );
747
748 let rule_config: TestRuleConfig = load_rule_config(&config);
750 assert!(rule_config.enabled); assert_eq!(rule_config.indent, 0); assert_eq!(rule_config.style, "custom"); assert_eq!(rule_config.items, Vec::<String>::new()); }
755
756 #[test]
757 fn test_conversion_roundtrip() {
758 let original = toml::Value::Table({
760 let mut table = toml::map::Map::new();
761 table.insert("string".to_string(), toml::Value::String("test".to_string()));
762 table.insert("number".to_string(), toml::Value::Integer(42));
763 table.insert("bool".to_string(), toml::Value::Boolean(true));
764 table.insert(
765 "array".to_string(),
766 toml::Value::Array(vec![
767 toml::Value::String("a".to_string()),
768 toml::Value::String("b".to_string()),
769 ]),
770 );
771 table
772 });
773
774 let json = toml_value_to_json(&original).unwrap();
775 let back_to_toml = json_to_toml_value(&json).unwrap();
776
777 assert_eq!(original, back_to_toml);
778 }
779
780 #[test]
781 fn test_edge_cases() {
782 let empty_arr = toml::Value::Array(vec![]);
784 let json_arr = toml_value_to_json(&empty_arr).unwrap();
785 assert_eq!(json_arr, serde_json::json!([]));
786
787 let empty_table = toml::Value::Table(toml::map::Map::new());
789 let json_table = toml_value_to_json(&empty_table).unwrap();
790 assert_eq!(json_table, serde_json::json!({}));
791
792 let nested = toml::Value::Table({
794 let mut outer = toml::map::Map::new();
795 outer.insert(
796 "inner".to_string(),
797 toml::Value::Table({
798 let mut inner = toml::map::Map::new();
799 inner.insert("value".to_string(), toml::Value::Integer(123));
800 inner
801 }),
802 );
803 outer
804 });
805 let json_nested = toml_value_to_json(&nested).unwrap();
806 assert_eq!(
807 json_nested,
808 serde_json::json!({
809 "inner": {
810 "value": 123
811 }
812 })
813 );
814 }
815
816 #[test]
817 fn test_float_edge_cases() {
818 let nan = serde_json::Number::from_f64(f64::NAN);
820 assert!(nan.is_none());
821
822 let inf = serde_json::Number::from_f64(f64::INFINITY);
823 assert!(inf.is_none());
824
825 let valid_float = toml::Value::Float(1.23);
827 let json_float = toml_value_to_json(&valid_float).unwrap();
828 assert_eq!(json_float, serde_json::json!(1.23));
829 }
830
831 #[test]
832 fn test_invalid_config_returns_default() {
833 let mut config = crate::config::Config::default();
835 let mut rule_values = BTreeMap::new();
836 rule_values.insert("unknown_field".to_string(), toml::Value::Boolean(true));
837 rule_values.insert("items".to_string(), toml::Value::Table(toml::map::Map::new()));
839
840 config.rules.insert(
841 "TEST001".to_string(),
842 crate::config::RuleConfig {
843 severity: None,
844 values: rule_values,
845 },
846 );
847
848 let rule_config: TestRuleConfig = load_rule_config(&config);
850 assert_eq!(rule_config, TestRuleConfig::default());
852 }
853
854 #[test]
855 fn test_invalid_field_type() {
856 let mut config = crate::config::Config::default();
858 let mut rule_values = BTreeMap::new();
859 rule_values.insert("indent".to_string(), toml::Value::String("not_a_number".to_string()));
861
862 config.rules.insert(
863 "TEST001".to_string(),
864 crate::config::RuleConfig {
865 severity: None,
866 values: rule_values,
867 },
868 );
869
870 let rule_config: TestRuleConfig = load_rule_config(&config);
872 assert_eq!(rule_config, TestRuleConfig::default());
873 }
874
875 #[test]
878 fn test_is_rule_name_valid() {
879 assert!(is_rule_name("MD001"));
881 assert!(is_rule_name("MD060"));
882 assert!(is_rule_name("MD123"));
883 assert!(is_rule_name("MD999"));
884
885 assert!(is_rule_name("md001"));
887 assert!(is_rule_name("Md060"));
888 assert!(is_rule_name("mD123"));
889
890 assert!(is_rule_name("MD0001"));
892 assert!(is_rule_name("MD12345"));
893 }
894
895 #[test]
896 fn test_is_rule_name_invalid() {
897 assert!(!is_rule_name("MD"));
899 assert!(!is_rule_name("MD1"));
900 assert!(!is_rule_name("M"));
901 assert!(!is_rule_name(""));
902
903 assert!(!is_rule_name("disable"));
905 assert!(!is_rule_name("enable"));
906 assert!(!is_rule_name("flavor"));
907 assert!(!is_rule_name("line-length"));
908 assert!(!is_rule_name("global"));
909
910 assert!(!is_rule_name("MDA01")); assert!(!is_rule_name("XD001")); assert!(!is_rule_name("MD00A")); assert!(!is_rule_name("1MD001")); assert!(!is_rule_name("MD-001")); }
917
918 #[test]
921 fn test_json_to_rule_config_simple() {
922 let json = serde_json::json!({
923 "enabled": true,
924 "style": "aligned"
925 });
926
927 let rule_config = json_to_rule_config(&json).unwrap();
928
929 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
930 assert_eq!(
931 rule_config.values.get("style"),
932 Some(&toml::Value::String("aligned".to_string()))
933 );
934 assert!(rule_config.severity.is_none());
935 }
936
937 #[test]
938 fn test_json_to_rule_config_with_numbers() {
939 let json = serde_json::json!({
940 "line-length": 120,
941 "max-width": 0,
942 "indent": 4
943 });
944
945 let rule_config = json_to_rule_config(&json).unwrap();
946
947 assert_eq!(rule_config.values.get("line-length"), Some(&toml::Value::Integer(120)));
948 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(0)));
949 assert_eq!(rule_config.values.get("indent"), Some(&toml::Value::Integer(4)));
950 }
951
952 #[test]
953 fn test_json_to_rule_config_with_arrays() {
954 let json = serde_json::json!({
955 "names": ["JavaScript", "TypeScript", "React"],
956 "exclude-patterns": ["*.test.md", "draft-*"]
957 });
958
959 let rule_config = json_to_rule_config(&json).unwrap();
960
961 let expected_names = toml::Value::Array(vec![
962 toml::Value::String("JavaScript".to_string()),
963 toml::Value::String("TypeScript".to_string()),
964 toml::Value::String("React".to_string()),
965 ]);
966 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
967
968 let expected_patterns = toml::Value::Array(vec![
969 toml::Value::String("*.test.md".to_string()),
970 toml::Value::String("draft-*".to_string()),
971 ]);
972 assert_eq!(rule_config.values.get("exclude-patterns"), Some(&expected_patterns));
973 }
974
975 #[test]
976 fn test_json_to_rule_config_with_severity() {
977 let json = serde_json::json!({
979 "severity": "error",
980 "style": "aligned"
981 });
982 let rule_config = json_to_rule_config(&json).unwrap();
983 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
984 assert!(!rule_config.values.contains_key("severity")); let json = serde_json::json!({
988 "severity": "warning",
989 "enabled": true
990 });
991 let rule_config = json_to_rule_config(&json).unwrap();
992 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Warning));
993
994 let json = serde_json::json!({
996 "severity": "info"
997 });
998 let rule_config = json_to_rule_config(&json).unwrap();
999 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Info));
1000
1001 let json = serde_json::json!({
1003 "severity": "ERROR"
1004 });
1005 let rule_config = json_to_rule_config(&json).unwrap();
1006 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
1007 }
1008
1009 #[test]
1010 fn test_json_to_rule_config_invalid_severity() {
1011 let json = serde_json::json!({
1013 "severity": "critical",
1014 "style": "aligned"
1015 });
1016 let rule_config = json_to_rule_config(&json).unwrap();
1017 assert!(rule_config.severity.is_none()); assert_eq!(
1019 rule_config.values.get("style"),
1020 Some(&toml::Value::String("aligned".to_string()))
1021 );
1022
1023 let json = serde_json::json!({
1025 "severity": 1,
1026 "enabled": true
1027 });
1028 let rule_config = json_to_rule_config(&json).unwrap();
1029 assert!(rule_config.severity.is_none()); }
1031
1032 #[test]
1033 fn test_json_to_rule_config_non_object() {
1034 assert!(json_to_rule_config(&serde_json::json!(42)).is_none());
1036 assert!(json_to_rule_config(&serde_json::json!("string")).is_none());
1037 assert!(json_to_rule_config(&serde_json::json!(true)).is_none());
1038 assert!(json_to_rule_config(&serde_json::json!([1, 2, 3])).is_none());
1039 assert!(json_to_rule_config(&serde_json::Value::Null).is_none());
1040 }
1041
1042 #[test]
1043 fn test_json_to_rule_config_empty_object() {
1044 let json = serde_json::json!({});
1045 let rule_config = json_to_rule_config(&json).unwrap();
1046 assert!(rule_config.values.is_empty());
1047 assert!(rule_config.severity.is_none());
1048 }
1049
1050 #[test]
1051 fn test_json_to_rule_config_nested_objects() {
1052 let json = serde_json::json!({
1054 "options": {
1055 "nested-key": "nested-value",
1056 "nested-number": 42
1057 }
1058 });
1059
1060 let rule_config = json_to_rule_config(&json).unwrap();
1061
1062 let options = rule_config.values.get("options").unwrap();
1063 if let toml::Value::Table(table) = options {
1064 assert_eq!(
1065 table.get("nested-key"),
1066 Some(&toml::Value::String("nested-value".to_string()))
1067 );
1068 assert_eq!(table.get("nested-number"), Some(&toml::Value::Integer(42)));
1069 } else {
1070 panic!("options should be a table");
1071 }
1072 }
1073
1074 #[test]
1075 fn test_json_to_rule_config_md060_example() {
1076 let json = serde_json::json!({
1078 "enabled": true,
1079 "style": "aligned",
1080 "max-width": 120,
1081 "column-align": "auto",
1082 "loose-last-column": false
1083 });
1084
1085 let rule_config = json_to_rule_config(&json).unwrap();
1086
1087 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
1088 assert_eq!(
1089 rule_config.values.get("style"),
1090 Some(&toml::Value::String("aligned".to_string()))
1091 );
1092 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(120)));
1093 assert_eq!(
1094 rule_config.values.get("column-align"),
1095 Some(&toml::Value::String("auto".to_string()))
1096 );
1097 assert_eq!(
1098 rule_config.values.get("loose-last-column"),
1099 Some(&toml::Value::Boolean(false))
1100 );
1101 }
1102
1103 #[test]
1104 fn test_json_to_rule_config_md044_example() {
1105 let json = serde_json::json!({
1107 "names": ["JavaScript", "TypeScript", "GitHub", "macOS"],
1108 "code-blocks": false,
1109 "html-elements": false
1110 });
1111
1112 let rule_config = json_to_rule_config(&json).unwrap();
1113
1114 let expected_names = toml::Value::Array(vec![
1115 toml::Value::String("JavaScript".to_string()),
1116 toml::Value::String("TypeScript".to_string()),
1117 toml::Value::String("GitHub".to_string()),
1118 toml::Value::String("macOS".to_string()),
1119 ]);
1120 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
1121 assert_eq!(
1122 rule_config.values.get("code-blocks"),
1123 Some(&toml::Value::Boolean(false))
1124 );
1125 assert_eq!(
1126 rule_config.values.get("html-elements"),
1127 Some(&toml::Value::Boolean(false))
1128 );
1129 }
1130
1131 #[test]
1134 fn test_json_to_rule_config_with_warnings_valid() {
1135 let json = serde_json::json!({
1136 "severity": "error",
1137 "enabled": true
1138 });
1139
1140 let result = json_to_rule_config_with_warnings(&json);
1141
1142 assert!(result.config.is_some());
1143 assert!(
1144 result.warnings.is_empty(),
1145 "Expected no warnings, got: {:?}",
1146 result.warnings
1147 );
1148 assert_eq!(result.config.unwrap().severity, Some(crate::rule::Severity::Error));
1149 }
1150
1151 #[test]
1152 fn test_json_to_rule_config_with_warnings_invalid_severity() {
1153 let json = serde_json::json!({
1154 "severity": "critical",
1155 "style": "aligned"
1156 });
1157
1158 let result = json_to_rule_config_with_warnings(&json);
1159
1160 assert!(result.config.is_some());
1161 assert_eq!(result.warnings.len(), 1);
1162 assert!(result.warnings[0].contains("Invalid severity 'critical'"));
1163 assert!(result.config.unwrap().severity.is_none());
1165 }
1166
1167 #[test]
1168 fn test_json_to_rule_config_with_warnings_wrong_severity_type() {
1169 let json = serde_json::json!({
1170 "severity": 123,
1171 "enabled": true
1172 });
1173
1174 let result = json_to_rule_config_with_warnings(&json);
1175
1176 assert!(result.config.is_some());
1177 assert_eq!(result.warnings.len(), 1);
1178 assert!(result.warnings[0].contains("Severity must be a string"));
1179 }
1180
1181 #[test]
1182 fn test_json_to_rule_config_with_warnings_non_object() {
1183 let json = serde_json::json!("not an object");
1184
1185 let result = json_to_rule_config_with_warnings(&json);
1186
1187 assert!(result.config.is_none());
1188 assert_eq!(result.warnings.len(), 1);
1189 assert!(result.warnings[0].contains("Expected object"));
1190 }
1191
1192 #[test]
1195 fn test_rule_config_integration_with_config() {
1196 let mut config = crate::config::Config::default();
1198
1199 let md060_json = serde_json::json!({
1201 "enabled": true,
1202 "style": "aligned",
1203 "max-width": 120
1204 });
1205 let md013_json = serde_json::json!({
1206 "line-length": 100,
1207 "code-blocks": false
1208 });
1209
1210 if let Some(md060_config) = json_to_rule_config(&md060_json) {
1211 config.rules.insert("MD060".to_string(), md060_config);
1212 }
1213 if let Some(md013_config) = json_to_rule_config(&md013_json) {
1214 config.rules.insert("MD013".to_string(), md013_config);
1215 }
1216
1217 assert!(config.rules.contains_key("MD060"));
1219 assert!(config.rules.contains_key("MD013"));
1220
1221 let md060 = config.rules.get("MD060").unwrap();
1223 assert_eq!(md060.values.get("enabled"), Some(&toml::Value::Boolean(true)));
1224 assert_eq!(
1225 md060.values.get("style"),
1226 Some(&toml::Value::String("aligned".to_string()))
1227 );
1228 assert_eq!(md060.values.get("max-width"), Some(&toml::Value::Integer(120)));
1229 }
1230
1231 #[test]
1232 fn test_rule_config_integration_with_severity() {
1233 let mut config = crate::config::Config::default();
1234
1235 let json = serde_json::json!({
1236 "severity": "error",
1237 "enabled": true
1238 });
1239
1240 if let Some(rule_config) = json_to_rule_config(&json) {
1241 config.rules.insert("MD041".to_string(), rule_config);
1242 }
1243
1244 let md041 = config.rules.get("MD041").unwrap();
1245 assert_eq!(md041.severity, Some(crate::rule::Severity::Error));
1246 }
1247
1248 #[test]
1249 fn test_rule_config_integration_case_normalization() {
1250 let mut config = crate::config::Config::default();
1252
1253 let json = serde_json::json!({ "enabled": true });
1254
1255 for rule_name in ["md060", "MD060", "Md060"] {
1257 if is_rule_name(rule_name)
1258 && let Some(rule_config) = json_to_rule_config(&json)
1259 {
1260 config.rules.insert(rule_name.to_ascii_uppercase(), rule_config);
1261 }
1262 }
1263
1264 assert!(config.rules.contains_key("MD060"));
1266 assert_eq!(config.rules.len(), 1); }
1268
1269 #[test]
1270 fn test_rule_config_integration_filters_non_rules() {
1271 let keys = ["MD060", "disable", "enable", "flavor", "line-length", "global"];
1273
1274 let rule_keys: Vec<_> = keys.iter().filter(|k| is_rule_name(k)).collect();
1275
1276 assert_eq!(rule_keys, vec![&"MD060"]);
1277 }
1278
1279 #[test]
1280 fn test_multiple_rule_configs_with_mixed_validity() {
1281 let rules = vec![
1283 ("MD060", serde_json::json!({ "severity": "error", "style": "aligned" })),
1284 (
1285 "MD013",
1286 serde_json::json!({ "severity": "invalid", "line-length": 100 }),
1287 ),
1288 ("MD041", serde_json::json!({ "enabled": true })),
1289 ];
1290
1291 let mut config = crate::config::Config::default();
1292 let mut all_warnings = Vec::new();
1293
1294 for (name, json) in rules {
1295 let result = json_to_rule_config_with_warnings(&json);
1296 all_warnings.extend(result.warnings);
1297 if let Some(rule_config) = result.config {
1298 config.rules.insert(name.to_string(), rule_config);
1299 }
1300 }
1301
1302 assert_eq!(config.rules.len(), 3);
1304
1305 assert_eq!(all_warnings.len(), 1);
1307 assert!(all_warnings[0].contains("Invalid severity"));
1308
1309 assert_eq!(
1311 config.rules.get("MD060").unwrap().severity,
1312 Some(crate::rule::Severity::Error)
1313 );
1314 assert!(config.rules.get("MD013").unwrap().severity.is_none());
1315 }
1316
1317 #[test]
1318 fn test_option_is_explicit() {
1319 let mut values = BTreeMap::new();
1320 values.insert("style".to_string(), toml::Value::String("indented".to_string()));
1321 let mut config = crate::config::Config::default();
1322 config.rules.insert(
1323 "MD046".to_string(),
1324 crate::config::RuleConfig { severity: None, values },
1325 );
1326
1327 assert!(option_is_explicit(&config, "MD046", "style"));
1328 assert!(!option_is_explicit(&config, "MD046", "language_required"));
1330 assert!(!option_is_explicit(&config, "MD048", "style"));
1331 }
1332
1333 #[test]
1334 fn test_flavor_override_notice_reports_once() {
1335 let notice = FlavorOverrideNotice::new();
1336 notice.report("MD046", "style", "indented", "fenced", "reason");
1337 assert!(notice.0.load(Ordering::Relaxed));
1338
1339 notice.report("MD046", "style", "indented", "fenced", "reason");
1341 assert!(!FlavorOverrideNotice::new().0.load(Ordering::Relaxed));
1342 }
1343
1344 #[test]
1346 fn test_flavor_override_notice_message_text() {
1347 assert_eq!(
1348 FlavorOverrideNotice::message(
1349 "MD046",
1350 "style",
1351 "indented",
1352 "fenced",
1353 "a Gherkin Doc String is only ever a backtick fence"
1354 ),
1355 "\x1b[33m[config warning]\x1b[0m MD046: Markdown with Gherkin flavor requires style=\"fenced\" \
1356 (a Gherkin Doc String is only ever a backtick fence). \
1357 Overriding style=\"indented\" to style=\"fenced\"."
1358 );
1359 assert_eq!(
1360 FlavorOverrideNotice::message(
1361 "MD048",
1362 "style",
1363 "tilde",
1364 "backtick",
1365 "a Gherkin Doc String is only ever a backtick fence"
1366 ),
1367 "\x1b[33m[config warning]\x1b[0m MD048: Markdown with Gherkin flavor requires style=\"backtick\" \
1368 (a Gherkin Doc String is only ever a backtick fence). \
1369 Overriding style=\"tilde\" to style=\"backtick\"."
1370 );
1371 assert_eq!(
1372 FlavorOverrideNotice::message(
1373 "MD055",
1374 "style",
1375 "no_leading_or_trailing",
1376 "leading_and_trailing",
1377 "a Gherkin table row is an indent followed directly by a pipe"
1378 ),
1379 "\x1b[33m[config warning]\x1b[0m MD055: Markdown with Gherkin flavor requires \
1380 style=\"leading_and_trailing\" \
1381 (a Gherkin table row is an indent followed directly by a pipe). \
1382 Overriding style=\"no_leading_or_trailing\" to style=\"leading_and_trailing\"."
1383 );
1384 }
1385
1386 #[test]
1390 fn test_end_to_end_md013_line_length_config() {
1391 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
1393
1394 let mut config = crate::config::Config::default();
1396 let json = serde_json::json!({
1397 "line-length": 40
1398 });
1399 if let Some(rule_config) = json_to_rule_config(&json) {
1400 config.rules.insert("MD013".to_string(), rule_config);
1401 }
1402
1403 config.global.enable = vec!["MD013".to_string()];
1405
1406 let rules = crate::rules::all_rules(&config);
1407 let filtered = crate::rules::filter_rules(&rules, &config.global);
1408
1409 let result = crate::lint(
1410 content,
1411 &filtered,
1412 false,
1413 crate::config::MarkdownFlavor::Standard,
1414 None,
1415 Some(&config),
1416 );
1417
1418 let warnings = result.expect("Linting should succeed");
1419
1420 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
1422 assert!(has_md013, "Should have MD013 warning with line-length=40");
1423 }
1424
1425 #[test]
1426 fn test_end_to_end_md013_line_length_no_warning() {
1427 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
1429
1430 let mut config = crate::config::Config::default();
1432 let json = serde_json::json!({
1433 "line-length": 100
1434 });
1435 if let Some(rule_config) = json_to_rule_config(&json) {
1436 config.rules.insert("MD013".to_string(), rule_config);
1437 }
1438
1439 config.global.enable = vec!["MD013".to_string()];
1441
1442 let rules = crate::rules::all_rules(&config);
1443 let filtered = crate::rules::filter_rules(&rules, &config.global);
1444
1445 let result = crate::lint(
1446 content,
1447 &filtered,
1448 false,
1449 crate::config::MarkdownFlavor::Standard,
1450 None,
1451 Some(&config),
1452 );
1453
1454 let warnings = result.expect("Linting should succeed");
1455
1456 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
1458 assert!(!has_md013, "Should NOT have MD013 warning with line-length=100");
1459 }
1460
1461 #[test]
1462 fn test_end_to_end_md044_proper_names() {
1463 let content = "# Test\n\nWe use javascript and typescript.\n";
1465
1466 let mut config = crate::config::Config::default();
1468 let json = serde_json::json!({
1469 "names": ["JavaScript", "TypeScript"],
1470 "code-blocks": false
1471 });
1472 if let Some(rule_config) = json_to_rule_config(&json) {
1473 config.rules.insert("MD044".to_string(), rule_config);
1474 }
1475
1476 config.global.enable = vec!["MD044".to_string()];
1478
1479 let rules = crate::rules::all_rules(&config);
1480 let filtered = crate::rules::filter_rules(&rules, &config.global);
1481
1482 let result = crate::lint(
1483 content,
1484 &filtered,
1485 false,
1486 crate::config::MarkdownFlavor::Standard,
1487 None,
1488 Some(&config),
1489 );
1490
1491 let warnings = result.expect("Linting should succeed");
1492
1493 let md044_warnings: Vec<_> = warnings
1495 .iter()
1496 .filter(|w| w.rule_name.as_deref() == Some("MD044"))
1497 .collect();
1498
1499 assert!(
1500 md044_warnings.len() >= 2,
1501 "Should have MD044 warnings for 'javascript' and 'typescript', got {}",
1502 md044_warnings.len()
1503 );
1504 }
1505
1506 #[test]
1507 fn test_end_to_end_severity_config() {
1508 let content = "test\n"; let mut config = crate::config::Config::default();
1512 let json = serde_json::json!({
1513 "severity": "info"
1514 });
1515 if let Some(rule_config) = json_to_rule_config(&json) {
1516 config.rules.insert("MD041".to_string(), rule_config);
1517 }
1518
1519 config.global.enable = vec!["MD041".to_string()];
1521
1522 let rules = crate::rules::all_rules(&config);
1523 let filtered = crate::rules::filter_rules(&rules, &config.global);
1524
1525 let result = crate::lint(
1526 content,
1527 &filtered,
1528 false,
1529 crate::config::MarkdownFlavor::Standard,
1530 None,
1531 Some(&config),
1532 );
1533
1534 let warnings = result.expect("Linting should succeed");
1535
1536 let md041 = warnings.iter().find(|w| w.rule_name.as_deref() == Some("MD041"));
1538 assert!(md041.is_some(), "Should have MD041 warning");
1539 assert_eq!(
1540 md041.unwrap().severity,
1541 crate::rule::Severity::Info,
1542 "MD041 should have Info severity from config"
1543 );
1544 }
1545}