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]
245macro_rules! impl_rule_config_schema {
246 ($config_ty:ty) => {
247 fn config_schema(&self) -> Option<(String, toml::Value)> {
248 $crate::rule_config_serde::config_schema_for::<$config_ty>()
249 }
250 };
251}
252
253#[macro_export]
257macro_rules! impl_rule_config_sections {
258 ($config_ty:ty) => {
259 fn default_config_section(&self) -> Option<(String, toml::Value)> {
260 $crate::rule_config_serde::default_config_section_for::<$config_ty>()
261 }
262
263 $crate::impl_rule_config_schema!($config_ty);
264 };
265}
266
267#[macro_export]
271macro_rules! impl_rule_config_methods {
272 ($config_ty:ty) => {
273 $crate::impl_rule_config_sections!($config_ty);
274
275 fn from_config(config: &$crate::config::Config) -> Box<dyn $crate::rule::Rule>
276 where
277 Self: Sized,
278 {
279 Box::new(Self::from_config_struct(
280 $crate::rule_config_serde::load_rule_config::<$config_ty>(config),
281 ))
282 }
283 };
284}
285
286pub fn json_to_toml_value(json_val: &serde_json::Value) -> Option<toml::Value> {
288 match json_val {
289 serde_json::Value::Null => None,
290 serde_json::Value::Bool(b) => Some(toml::Value::Boolean(*b)),
291 serde_json::Value::Number(n) => {
292 if let Some(i) = n.as_i64() {
293 Some(toml::Value::Integer(i))
294 } else {
295 n.as_f64().map(toml::Value::Float)
296 }
297 }
298 serde_json::Value::String(s) => Some(toml::Value::String(s.clone())),
299 serde_json::Value::Array(arr) => {
300 let toml_arr: Vec<_> = arr.iter().filter_map(json_to_toml_value).collect();
301 Some(toml::Value::Array(toml_arr))
302 }
303 serde_json::Value::Object(obj) => {
304 let mut toml_table = toml::map::Map::new();
305 for (k, v) in obj {
306 if let Some(toml_v) = json_to_toml_value(v) {
307 toml_table.insert(k.clone(), toml_v);
308 }
309 }
310 Some(toml::Value::Table(toml_table))
311 }
312 }
313}
314
315pub fn is_rule_name(name: &str) -> bool {
319 let upper = name.to_ascii_uppercase();
320 upper.starts_with("MD") && upper.len() >= 4 && upper[2..].chars().all(|c| c.is_ascii_digit())
321}
322
323#[derive(Debug, Default)]
325pub struct RuleConfigConversion {
326 pub config: Option<crate::config::RuleConfig>,
328 pub warnings: Vec<String>,
330}
331
332pub fn json_to_rule_config(json_value: &serde_json::Value) -> Option<crate::config::RuleConfig> {
340 json_to_rule_config_with_warnings(json_value).config
341}
342
343pub fn json_to_rule_config_with_warnings(json_value: &serde_json::Value) -> RuleConfigConversion {
348 use std::collections::BTreeMap;
349
350 let mut result = RuleConfigConversion::default();
351
352 let Some(obj) = json_value.as_object() else {
353 result.warnings.push(format!(
354 "Expected object for rule config, got {}",
355 json_type_name(json_value)
356 ));
357 return result;
358 };
359
360 let mut values = BTreeMap::new();
361 let mut severity = None;
362
363 for (key, val) in obj {
364 if key == "severity" {
366 if let Some(s) = val.as_str() {
367 match s.to_lowercase().as_str() {
368 "error" => severity = Some(crate::rule::Severity::Error),
369 "warning" => severity = Some(crate::rule::Severity::Warning),
370 "info" => severity = Some(crate::rule::Severity::Info),
371 _ => {
372 result.warnings.push(format!(
373 "Invalid severity '{s}', expected 'error', 'warning', or 'info'"
374 ));
375 }
376 }
377 } else {
378 result
379 .warnings
380 .push(format!("Severity must be a string, got {}", json_type_name(val)));
381 }
382 continue;
383 }
384
385 if let Some(toml_val) = json_to_toml_value(val) {
387 values.insert(key.clone(), toml_val);
388 } else if !val.is_null() {
389 result
390 .warnings
391 .push(format!("Could not convert '{key}' value to config format"));
392 }
393 }
394
395 result.config = Some(crate::config::RuleConfig { severity, values });
396 result
397}
398
399fn json_type_name(val: &serde_json::Value) -> &'static str {
401 match val {
402 serde_json::Value::Null => "null",
403 serde_json::Value::Bool(_) => "boolean",
404 serde_json::Value::Number(_) => "number",
405 serde_json::Value::String(_) => "string",
406 serde_json::Value::Array(_) => "array",
407 serde_json::Value::Object(_) => "object",
408 }
409}
410
411pub fn toml_value_to_json(toml_val: &toml::Value) -> Option<serde_json::Value> {
413 match toml_val {
414 toml::Value::String(s) => Some(serde_json::Value::String(s.clone())),
415 toml::Value::Integer(i) => Some(serde_json::json!(i)),
416 toml::Value::Float(f) => Some(serde_json::json!(f)),
417 toml::Value::Boolean(b) => Some(serde_json::Value::Bool(*b)),
418 toml::Value::Array(arr) => {
419 let json_arr: Vec<_> = arr.iter().filter_map(toml_value_to_json).collect();
420 Some(serde_json::Value::Array(json_arr))
421 }
422 toml::Value::Table(table) => {
423 let mut json_obj = serde_json::Map::new();
424 for (k, v) in table {
425 if let Some(json_v) = toml_value_to_json(v) {
426 json_obj.insert(k.clone(), json_v);
427 }
428 }
429 Some(serde_json::Value::Object(json_obj))
430 }
431 toml::Value::Datetime(_) => None, }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use serde::{Deserialize, Serialize};
439 use std::collections::BTreeMap;
440
441 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
443 #[serde(default)]
444 struct TestRuleConfig {
445 #[serde(default)]
446 enabled: bool,
447 #[serde(default)]
448 indent: i64,
449 #[serde(default)]
450 style: String,
451 #[serde(default)]
452 items: Vec<String>,
453 }
454
455 impl RuleConfig for TestRuleConfig {
456 const RULE_NAME: &'static str = "TEST001";
457 }
458
459 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
463 #[serde(default)]
464 struct NullableTestConfig {
465 #[serde(default)]
466 enabled: bool,
467 #[serde(default, alias = "key-order")]
468 key_order: Option<Vec<String>>,
469 #[serde(default, alias = "title-pattern")]
470 title_pattern: Option<String>,
471 }
472
473 impl RuleConfig for NullableTestConfig {
474 const RULE_NAME: &'static str = "TEST_NULLABLE";
475 }
476
477 #[test]
478 fn test_is_nullable_sentinel() {
479 let sentinel = toml::Value::String(NULLABLE_SENTINEL.to_string());
480 assert!(is_nullable_sentinel(&sentinel));
481
482 let regular = toml::Value::String("normal".to_string());
483 assert!(!is_nullable_sentinel(®ular));
484
485 let integer = toml::Value::Integer(42);
486 assert!(!is_nullable_sentinel(&integer));
487 }
488
489 #[test]
490 fn test_is_polymorphic_sentinel() {
491 let sentinel = polymorphic_sentinel_value();
492 assert!(is_polymorphic_sentinel(&sentinel));
493
494 let nullable = toml::Value::String(NULLABLE_SENTINEL.to_string());
496 assert!(!is_polymorphic_sentinel(&nullable));
497 assert!(!is_nullable_sentinel(&sentinel));
498
499 let regular = toml::Value::String("normal".to_string());
500 assert!(!is_polymorphic_sentinel(®ular));
501 }
502
503 #[test]
504 fn test_config_schema_table_preserves_nullable_keys() {
505 let config = NullableTestConfig::default();
506 let table = config_schema_table(&config).unwrap();
507
508 assert!(table.contains_key("enabled"), "enabled key missing");
510 assert!(table.contains_key("key_order"), "key_order key missing");
511 assert!(table.contains_key("title_pattern"), "title_pattern key missing");
512
513 assert!(is_nullable_sentinel(table.get("key_order").unwrap()));
515 assert!(is_nullable_sentinel(table.get("title_pattern").unwrap()));
516
517 assert_eq!(table.get("enabled"), Some(&toml::Value::Boolean(false)));
519 }
520
521 #[test]
522 fn test_config_schema_table_non_null_option_uses_real_value() {
523 let config = NullableTestConfig {
524 enabled: true,
525 key_order: Some(vec!["title".to_string(), "date".to_string()]),
526 title_pattern: Some("pattern".to_string()),
527 };
528 let table = config_schema_table(&config).unwrap();
529
530 let key_order = table.get("key_order").unwrap();
532 assert!(!is_nullable_sentinel(key_order));
533 assert!(matches!(key_order, toml::Value::Array(_)));
534
535 let title_pattern = table.get("title_pattern").unwrap();
536 assert!(!is_nullable_sentinel(title_pattern));
537 assert_eq!(title_pattern, &toml::Value::String("pattern".to_string()));
538 }
539
540 #[test]
541 fn test_json_to_toml_value_still_drops_null() {
542 assert!(json_to_toml_value(&serde_json::Value::Null).is_none());
544 }
545
546 #[test]
547 fn test_config_schema_table_all_keys_present() {
548 let config = NullableTestConfig::default();
549 let table = config_schema_table(&config).unwrap();
550 assert_eq!(table.len(), 3, "Expected 3 keys: enabled, key_order, title_pattern");
551 }
552
553 #[test]
554 fn test_config_schema_table_never_drops_keys() {
555 let mut obj = serde_json::Map::new();
559 obj.insert("real_key".to_string(), serde_json::json!(42));
560 obj.insert("null_key".to_string(), serde_json::Value::Null);
561 let json = serde_json::Value::Object(obj);
562
563 let obj = json.as_object().unwrap();
565 let mut table = toml::map::Map::new();
566 for (k, v) in obj {
567 if v.is_null() {
568 table.insert(k.clone(), toml::Value::String(NULLABLE_SENTINEL.to_string()));
569 } else {
570 let toml_v =
571 json_to_toml_value(v).unwrap_or_else(|| toml::Value::String(NULLABLE_SENTINEL.to_string()));
572 table.insert(k.clone(), toml_v);
573 }
574 }
575
576 assert_eq!(table.len(), 2, "Both keys must be present");
577 assert!(table.contains_key("real_key"));
578 assert!(table.contains_key("null_key"));
579 }
580
581 #[test]
582 fn test_toml_value_to_json_basic_types() {
583 let toml_str = toml::Value::String("hello".to_string());
585 let json_str = toml_value_to_json(&toml_str).unwrap();
586 assert_eq!(json_str, serde_json::Value::String("hello".to_string()));
587
588 let toml_int = toml::Value::Integer(42);
590 let json_int = toml_value_to_json(&toml_int).unwrap();
591 assert_eq!(json_int, serde_json::json!(42));
592
593 let toml_float = toml::Value::Float(1.234);
595 let json_float = toml_value_to_json(&toml_float).unwrap();
596 assert_eq!(json_float, serde_json::json!(1.234));
597
598 let toml_bool = toml::Value::Boolean(true);
600 let json_bool = toml_value_to_json(&toml_bool).unwrap();
601 assert_eq!(json_bool, serde_json::Value::Bool(true));
602 }
603
604 #[test]
605 fn test_toml_value_to_json_complex_types() {
606 let toml_arr = toml::Value::Array(vec![
608 toml::Value::String("a".to_string()),
609 toml::Value::String("b".to_string()),
610 ]);
611 let json_arr = toml_value_to_json(&toml_arr).unwrap();
612 assert_eq!(json_arr, serde_json::json!(["a", "b"]));
613
614 let mut toml_table = toml::map::Map::new();
616 toml_table.insert("key1".to_string(), toml::Value::String("value1".to_string()));
617 toml_table.insert("key2".to_string(), toml::Value::Integer(123));
618 let toml_tbl = toml::Value::Table(toml_table);
619 let json_tbl = toml_value_to_json(&toml_tbl).unwrap();
620
621 let expected = serde_json::json!({
622 "key1": "value1",
623 "key2": 123
624 });
625 assert_eq!(json_tbl, expected);
626 }
627
628 #[test]
629 fn test_toml_value_to_json_datetime() {
630 let toml_dt = toml::Value::Datetime("2023-01-01T00:00:00Z".parse().unwrap());
632 assert!(toml_value_to_json(&toml_dt).is_none());
633 }
634
635 #[test]
636 fn test_json_to_toml_value_basic_types() {
637 assert!(json_to_toml_value(&serde_json::Value::Null).is_none());
639
640 let json_bool = serde_json::Value::Bool(false);
642 let toml_bool = json_to_toml_value(&json_bool).unwrap();
643 assert_eq!(toml_bool, toml::Value::Boolean(false));
644
645 let json_int = serde_json::json!(42);
647 let toml_int = json_to_toml_value(&json_int).unwrap();
648 assert_eq!(toml_int, toml::Value::Integer(42));
649
650 let json_float = serde_json::json!(1.234);
652 let toml_float = json_to_toml_value(&json_float).unwrap();
653 assert_eq!(toml_float, toml::Value::Float(1.234));
654
655 let json_str = serde_json::Value::String("test".to_string());
657 let toml_str = json_to_toml_value(&json_str).unwrap();
658 assert_eq!(toml_str, toml::Value::String("test".to_string()));
659 }
660
661 #[test]
662 fn test_json_to_toml_value_complex_types() {
663 let json_arr = serde_json::json!(["x", "y", "z"]);
665 let toml_arr = json_to_toml_value(&json_arr).unwrap();
666 if let toml::Value::Array(arr) = toml_arr {
667 assert_eq!(arr.len(), 3);
668 assert_eq!(arr[0], toml::Value::String("x".to_string()));
669 assert_eq!(arr[1], toml::Value::String("y".to_string()));
670 assert_eq!(arr[2], toml::Value::String("z".to_string()));
671 } else {
672 panic!("Expected array");
673 }
674
675 let json_obj = serde_json::json!({
677 "name": "test",
678 "count": 10,
679 "active": true
680 });
681 let toml_obj = json_to_toml_value(&json_obj).unwrap();
682 if let toml::Value::Table(table) = toml_obj {
683 assert_eq!(table.get("name"), Some(&toml::Value::String("test".to_string())));
684 assert_eq!(table.get("count"), Some(&toml::Value::Integer(10)));
685 assert_eq!(table.get("active"), Some(&toml::Value::Boolean(true)));
686 } else {
687 panic!("Expected table");
688 }
689 }
690
691 #[test]
692 fn test_load_rule_config_default() {
693 let config = crate::config::Config::default();
695
696 let rule_config: TestRuleConfig = load_rule_config(&config);
698 assert_eq!(rule_config, TestRuleConfig::default());
699 }
700
701 #[test]
702 fn test_load_rule_config_with_values() {
703 let mut config = crate::config::Config::default();
705 let mut rule_values = BTreeMap::new();
706 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
707 rule_values.insert("indent".to_string(), toml::Value::Integer(4));
708 rule_values.insert("style".to_string(), toml::Value::String("consistent".to_string()));
709 rule_values.insert(
710 "items".to_string(),
711 toml::Value::Array(vec![
712 toml::Value::String("item1".to_string()),
713 toml::Value::String("item2".to_string()),
714 ]),
715 );
716
717 config.rules.insert(
718 "TEST001".to_string(),
719 crate::config::RuleConfig {
720 severity: None,
721 values: rule_values,
722 },
723 );
724
725 let rule_config: TestRuleConfig = load_rule_config(&config);
727 assert!(rule_config.enabled);
728 assert_eq!(rule_config.indent, 4);
729 assert_eq!(rule_config.style, "consistent");
730 assert_eq!(rule_config.items, vec!["item1", "item2"]);
731 }
732
733 #[test]
734 fn test_load_rule_config_partial() {
735 let mut config = crate::config::Config::default();
737 let mut rule_values = BTreeMap::new();
738 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
739 rule_values.insert("style".to_string(), toml::Value::String("custom".to_string()));
740
741 config.rules.insert(
742 "TEST001".to_string(),
743 crate::config::RuleConfig {
744 severity: None,
745 values: rule_values,
746 },
747 );
748
749 let rule_config: TestRuleConfig = load_rule_config(&config);
751 assert!(rule_config.enabled); assert_eq!(rule_config.indent, 0); assert_eq!(rule_config.style, "custom"); assert_eq!(rule_config.items, Vec::<String>::new()); }
756
757 #[test]
758 fn test_conversion_roundtrip() {
759 let original = toml::Value::Table({
761 let mut table = toml::map::Map::new();
762 table.insert("string".to_string(), toml::Value::String("test".to_string()));
763 table.insert("number".to_string(), toml::Value::Integer(42));
764 table.insert("bool".to_string(), toml::Value::Boolean(true));
765 table.insert(
766 "array".to_string(),
767 toml::Value::Array(vec![
768 toml::Value::String("a".to_string()),
769 toml::Value::String("b".to_string()),
770 ]),
771 );
772 table
773 });
774
775 let json = toml_value_to_json(&original).unwrap();
776 let back_to_toml = json_to_toml_value(&json).unwrap();
777
778 assert_eq!(original, back_to_toml);
779 }
780
781 #[test]
782 fn test_edge_cases() {
783 let empty_arr = toml::Value::Array(vec![]);
785 let json_arr = toml_value_to_json(&empty_arr).unwrap();
786 assert_eq!(json_arr, serde_json::json!([]));
787
788 let empty_table = toml::Value::Table(toml::map::Map::new());
790 let json_table = toml_value_to_json(&empty_table).unwrap();
791 assert_eq!(json_table, serde_json::json!({}));
792
793 let nested = toml::Value::Table({
795 let mut outer = toml::map::Map::new();
796 outer.insert(
797 "inner".to_string(),
798 toml::Value::Table({
799 let mut inner = toml::map::Map::new();
800 inner.insert("value".to_string(), toml::Value::Integer(123));
801 inner
802 }),
803 );
804 outer
805 });
806 let json_nested = toml_value_to_json(&nested).unwrap();
807 assert_eq!(
808 json_nested,
809 serde_json::json!({
810 "inner": {
811 "value": 123
812 }
813 })
814 );
815 }
816
817 #[test]
818 fn test_float_edge_cases() {
819 let nan = serde_json::Number::from_f64(f64::NAN);
821 assert!(nan.is_none());
822
823 let inf = serde_json::Number::from_f64(f64::INFINITY);
824 assert!(inf.is_none());
825
826 let valid_float = toml::Value::Float(1.23);
828 let json_float = toml_value_to_json(&valid_float).unwrap();
829 assert_eq!(json_float, serde_json::json!(1.23));
830 }
831
832 #[test]
833 fn test_invalid_config_returns_default() {
834 let mut config = crate::config::Config::default();
836 let mut rule_values = BTreeMap::new();
837 rule_values.insert("unknown_field".to_string(), toml::Value::Boolean(true));
838 rule_values.insert("items".to_string(), toml::Value::Table(toml::map::Map::new()));
840
841 config.rules.insert(
842 "TEST001".to_string(),
843 crate::config::RuleConfig {
844 severity: None,
845 values: rule_values,
846 },
847 );
848
849 let rule_config: TestRuleConfig = load_rule_config(&config);
851 assert_eq!(rule_config, TestRuleConfig::default());
853 }
854
855 #[test]
856 fn test_invalid_field_type() {
857 let mut config = crate::config::Config::default();
859 let mut rule_values = BTreeMap::new();
860 rule_values.insert("indent".to_string(), toml::Value::String("not_a_number".to_string()));
862
863 config.rules.insert(
864 "TEST001".to_string(),
865 crate::config::RuleConfig {
866 severity: None,
867 values: rule_values,
868 },
869 );
870
871 let rule_config: TestRuleConfig = load_rule_config(&config);
873 assert_eq!(rule_config, TestRuleConfig::default());
874 }
875
876 #[test]
879 fn test_is_rule_name_valid() {
880 assert!(is_rule_name("MD001"));
882 assert!(is_rule_name("MD060"));
883 assert!(is_rule_name("MD123"));
884 assert!(is_rule_name("MD999"));
885
886 assert!(is_rule_name("md001"));
888 assert!(is_rule_name("Md060"));
889 assert!(is_rule_name("mD123"));
890
891 assert!(is_rule_name("MD0001"));
893 assert!(is_rule_name("MD12345"));
894 }
895
896 #[test]
897 fn test_is_rule_name_invalid() {
898 assert!(!is_rule_name("MD"));
900 assert!(!is_rule_name("MD1"));
901 assert!(!is_rule_name("M"));
902 assert!(!is_rule_name(""));
903
904 assert!(!is_rule_name("disable"));
906 assert!(!is_rule_name("enable"));
907 assert!(!is_rule_name("flavor"));
908 assert!(!is_rule_name("line-length"));
909 assert!(!is_rule_name("global"));
910
911 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")); }
918
919 #[test]
922 fn test_json_to_rule_config_simple() {
923 let json = serde_json::json!({
924 "enabled": true,
925 "style": "aligned"
926 });
927
928 let rule_config = json_to_rule_config(&json).unwrap();
929
930 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
931 assert_eq!(
932 rule_config.values.get("style"),
933 Some(&toml::Value::String("aligned".to_string()))
934 );
935 assert!(rule_config.severity.is_none());
936 }
937
938 #[test]
939 fn test_json_to_rule_config_with_numbers() {
940 let json = serde_json::json!({
941 "line-length": 120,
942 "max-width": 0,
943 "indent": 4
944 });
945
946 let rule_config = json_to_rule_config(&json).unwrap();
947
948 assert_eq!(rule_config.values.get("line-length"), Some(&toml::Value::Integer(120)));
949 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(0)));
950 assert_eq!(rule_config.values.get("indent"), Some(&toml::Value::Integer(4)));
951 }
952
953 #[test]
954 fn test_json_to_rule_config_with_arrays() {
955 let json = serde_json::json!({
956 "names": ["JavaScript", "TypeScript", "React"],
957 "exclude-patterns": ["*.test.md", "draft-*"]
958 });
959
960 let rule_config = json_to_rule_config(&json).unwrap();
961
962 let expected_names = toml::Value::Array(vec![
963 toml::Value::String("JavaScript".to_string()),
964 toml::Value::String("TypeScript".to_string()),
965 toml::Value::String("React".to_string()),
966 ]);
967 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
968
969 let expected_patterns = toml::Value::Array(vec![
970 toml::Value::String("*.test.md".to_string()),
971 toml::Value::String("draft-*".to_string()),
972 ]);
973 assert_eq!(rule_config.values.get("exclude-patterns"), Some(&expected_patterns));
974 }
975
976 #[test]
977 fn test_json_to_rule_config_with_severity() {
978 let json = serde_json::json!({
980 "severity": "error",
981 "style": "aligned"
982 });
983 let rule_config = json_to_rule_config(&json).unwrap();
984 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
985 assert!(!rule_config.values.contains_key("severity")); let json = serde_json::json!({
989 "severity": "warning",
990 "enabled": true
991 });
992 let rule_config = json_to_rule_config(&json).unwrap();
993 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Warning));
994
995 let json = serde_json::json!({
997 "severity": "info"
998 });
999 let rule_config = json_to_rule_config(&json).unwrap();
1000 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Info));
1001
1002 let json = serde_json::json!({
1004 "severity": "ERROR"
1005 });
1006 let rule_config = json_to_rule_config(&json).unwrap();
1007 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
1008 }
1009
1010 #[test]
1011 fn test_json_to_rule_config_invalid_severity() {
1012 let json = serde_json::json!({
1014 "severity": "critical",
1015 "style": "aligned"
1016 });
1017 let rule_config = json_to_rule_config(&json).unwrap();
1018 assert!(rule_config.severity.is_none()); assert_eq!(
1020 rule_config.values.get("style"),
1021 Some(&toml::Value::String("aligned".to_string()))
1022 );
1023
1024 let json = serde_json::json!({
1026 "severity": 1,
1027 "enabled": true
1028 });
1029 let rule_config = json_to_rule_config(&json).unwrap();
1030 assert!(rule_config.severity.is_none()); }
1032
1033 #[test]
1034 fn test_json_to_rule_config_non_object() {
1035 assert!(json_to_rule_config(&serde_json::json!(42)).is_none());
1037 assert!(json_to_rule_config(&serde_json::json!("string")).is_none());
1038 assert!(json_to_rule_config(&serde_json::json!(true)).is_none());
1039 assert!(json_to_rule_config(&serde_json::json!([1, 2, 3])).is_none());
1040 assert!(json_to_rule_config(&serde_json::Value::Null).is_none());
1041 }
1042
1043 #[test]
1044 fn test_json_to_rule_config_empty_object() {
1045 let json = serde_json::json!({});
1046 let rule_config = json_to_rule_config(&json).unwrap();
1047 assert!(rule_config.values.is_empty());
1048 assert!(rule_config.severity.is_none());
1049 }
1050
1051 #[test]
1052 fn test_json_to_rule_config_nested_objects() {
1053 let json = serde_json::json!({
1055 "options": {
1056 "nested-key": "nested-value",
1057 "nested-number": 42
1058 }
1059 });
1060
1061 let rule_config = json_to_rule_config(&json).unwrap();
1062
1063 let options = rule_config.values.get("options").unwrap();
1064 if let toml::Value::Table(table) = options {
1065 assert_eq!(
1066 table.get("nested-key"),
1067 Some(&toml::Value::String("nested-value".to_string()))
1068 );
1069 assert_eq!(table.get("nested-number"), Some(&toml::Value::Integer(42)));
1070 } else {
1071 panic!("options should be a table");
1072 }
1073 }
1074
1075 #[test]
1076 fn test_json_to_rule_config_md060_example() {
1077 let json = serde_json::json!({
1079 "enabled": true,
1080 "style": "aligned",
1081 "max-width": 120,
1082 "column-align": "auto",
1083 "loose-last-column": false
1084 });
1085
1086 let rule_config = json_to_rule_config(&json).unwrap();
1087
1088 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
1089 assert_eq!(
1090 rule_config.values.get("style"),
1091 Some(&toml::Value::String("aligned".to_string()))
1092 );
1093 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(120)));
1094 assert_eq!(
1095 rule_config.values.get("column-align"),
1096 Some(&toml::Value::String("auto".to_string()))
1097 );
1098 assert_eq!(
1099 rule_config.values.get("loose-last-column"),
1100 Some(&toml::Value::Boolean(false))
1101 );
1102 }
1103
1104 #[test]
1105 fn test_json_to_rule_config_md044_example() {
1106 let json = serde_json::json!({
1108 "names": ["JavaScript", "TypeScript", "GitHub", "macOS"],
1109 "code-blocks": false,
1110 "html-elements": false
1111 });
1112
1113 let rule_config = json_to_rule_config(&json).unwrap();
1114
1115 let expected_names = toml::Value::Array(vec![
1116 toml::Value::String("JavaScript".to_string()),
1117 toml::Value::String("TypeScript".to_string()),
1118 toml::Value::String("GitHub".to_string()),
1119 toml::Value::String("macOS".to_string()),
1120 ]);
1121 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
1122 assert_eq!(
1123 rule_config.values.get("code-blocks"),
1124 Some(&toml::Value::Boolean(false))
1125 );
1126 assert_eq!(
1127 rule_config.values.get("html-elements"),
1128 Some(&toml::Value::Boolean(false))
1129 );
1130 }
1131
1132 #[test]
1135 fn test_json_to_rule_config_with_warnings_valid() {
1136 let json = serde_json::json!({
1137 "severity": "error",
1138 "enabled": true
1139 });
1140
1141 let result = json_to_rule_config_with_warnings(&json);
1142
1143 assert!(result.config.is_some());
1144 assert!(
1145 result.warnings.is_empty(),
1146 "Expected no warnings, got: {:?}",
1147 result.warnings
1148 );
1149 assert_eq!(result.config.unwrap().severity, Some(crate::rule::Severity::Error));
1150 }
1151
1152 #[test]
1153 fn test_json_to_rule_config_with_warnings_invalid_severity() {
1154 let json = serde_json::json!({
1155 "severity": "critical",
1156 "style": "aligned"
1157 });
1158
1159 let result = json_to_rule_config_with_warnings(&json);
1160
1161 assert!(result.config.is_some());
1162 assert_eq!(result.warnings.len(), 1);
1163 assert!(result.warnings[0].contains("Invalid severity 'critical'"));
1164 assert!(result.config.unwrap().severity.is_none());
1166 }
1167
1168 #[test]
1169 fn test_json_to_rule_config_with_warnings_wrong_severity_type() {
1170 let json = serde_json::json!({
1171 "severity": 123,
1172 "enabled": true
1173 });
1174
1175 let result = json_to_rule_config_with_warnings(&json);
1176
1177 assert!(result.config.is_some());
1178 assert_eq!(result.warnings.len(), 1);
1179 assert!(result.warnings[0].contains("Severity must be a string"));
1180 }
1181
1182 #[test]
1183 fn test_json_to_rule_config_with_warnings_non_object() {
1184 let json = serde_json::json!("not an object");
1185
1186 let result = json_to_rule_config_with_warnings(&json);
1187
1188 assert!(result.config.is_none());
1189 assert_eq!(result.warnings.len(), 1);
1190 assert!(result.warnings[0].contains("Expected object"));
1191 }
1192
1193 #[test]
1196 fn test_rule_config_integration_with_config() {
1197 let mut config = crate::config::Config::default();
1199
1200 let md060_json = serde_json::json!({
1202 "enabled": true,
1203 "style": "aligned",
1204 "max-width": 120
1205 });
1206 let md013_json = serde_json::json!({
1207 "line-length": 100,
1208 "code-blocks": false
1209 });
1210
1211 if let Some(md060_config) = json_to_rule_config(&md060_json) {
1212 config.rules.insert("MD060".to_string(), md060_config);
1213 }
1214 if let Some(md013_config) = json_to_rule_config(&md013_json) {
1215 config.rules.insert("MD013".to_string(), md013_config);
1216 }
1217
1218 assert!(config.rules.contains_key("MD060"));
1220 assert!(config.rules.contains_key("MD013"));
1221
1222 let md060 = config.rules.get("MD060").unwrap();
1224 assert_eq!(md060.values.get("enabled"), Some(&toml::Value::Boolean(true)));
1225 assert_eq!(
1226 md060.values.get("style"),
1227 Some(&toml::Value::String("aligned".to_string()))
1228 );
1229 assert_eq!(md060.values.get("max-width"), Some(&toml::Value::Integer(120)));
1230 }
1231
1232 #[test]
1233 fn test_rule_config_integration_with_severity() {
1234 let mut config = crate::config::Config::default();
1235
1236 let json = serde_json::json!({
1237 "severity": "error",
1238 "enabled": true
1239 });
1240
1241 if let Some(rule_config) = json_to_rule_config(&json) {
1242 config.rules.insert("MD041".to_string(), rule_config);
1243 }
1244
1245 let md041 = config.rules.get("MD041").unwrap();
1246 assert_eq!(md041.severity, Some(crate::rule::Severity::Error));
1247 }
1248
1249 #[test]
1250 fn test_rule_config_integration_case_normalization() {
1251 let mut config = crate::config::Config::default();
1253
1254 let json = serde_json::json!({ "enabled": true });
1255
1256 for rule_name in ["md060", "MD060", "Md060"] {
1258 if is_rule_name(rule_name)
1259 && let Some(rule_config) = json_to_rule_config(&json)
1260 {
1261 config.rules.insert(rule_name.to_ascii_uppercase(), rule_config);
1262 }
1263 }
1264
1265 assert!(config.rules.contains_key("MD060"));
1267 assert_eq!(config.rules.len(), 1); }
1269
1270 #[test]
1271 fn test_rule_config_integration_filters_non_rules() {
1272 let keys = ["MD060", "disable", "enable", "flavor", "line-length", "global"];
1274
1275 let rule_keys: Vec<_> = keys.iter().filter(|k| is_rule_name(k)).collect();
1276
1277 assert_eq!(rule_keys, vec![&"MD060"]);
1278 }
1279
1280 #[test]
1281 fn test_multiple_rule_configs_with_mixed_validity() {
1282 let rules = vec![
1284 ("MD060", serde_json::json!({ "severity": "error", "style": "aligned" })),
1285 (
1286 "MD013",
1287 serde_json::json!({ "severity": "invalid", "line-length": 100 }),
1288 ),
1289 ("MD041", serde_json::json!({ "enabled": true })),
1290 ];
1291
1292 let mut config = crate::config::Config::default();
1293 let mut all_warnings = Vec::new();
1294
1295 for (name, json) in rules {
1296 let result = json_to_rule_config_with_warnings(&json);
1297 all_warnings.extend(result.warnings);
1298 if let Some(rule_config) = result.config {
1299 config.rules.insert(name.to_string(), rule_config);
1300 }
1301 }
1302
1303 assert_eq!(config.rules.len(), 3);
1305
1306 assert_eq!(all_warnings.len(), 1);
1308 assert!(all_warnings[0].contains("Invalid severity"));
1309
1310 assert_eq!(
1312 config.rules.get("MD060").unwrap().severity,
1313 Some(crate::rule::Severity::Error)
1314 );
1315 assert!(config.rules.get("MD013").unwrap().severity.is_none());
1316 }
1317
1318 #[test]
1319 fn test_option_is_explicit() {
1320 let mut values = BTreeMap::new();
1321 values.insert("style".to_string(), toml::Value::String("indented".to_string()));
1322 let mut config = crate::config::Config::default();
1323 config.rules.insert(
1324 "MD046".to_string(),
1325 crate::config::RuleConfig { severity: None, values },
1326 );
1327
1328 assert!(option_is_explicit(&config, "MD046", "style"));
1329 assert!(!option_is_explicit(&config, "MD046", "language_required"));
1331 assert!(!option_is_explicit(&config, "MD048", "style"));
1332 }
1333
1334 #[test]
1335 fn test_flavor_override_notice_reports_once() {
1336 let notice = FlavorOverrideNotice::new();
1337 notice.report("MD046", "style", "indented", "fenced", "reason");
1338 assert!(notice.0.load(Ordering::Relaxed));
1339
1340 notice.report("MD046", "style", "indented", "fenced", "reason");
1342 assert!(!FlavorOverrideNotice::new().0.load(Ordering::Relaxed));
1343 }
1344
1345 #[test]
1347 fn test_flavor_override_notice_message_text() {
1348 assert_eq!(
1349 FlavorOverrideNotice::message(
1350 "MD046",
1351 "style",
1352 "indented",
1353 "fenced",
1354 "a Gherkin Doc String is only ever a backtick fence"
1355 ),
1356 "\x1b[33m[config warning]\x1b[0m MD046: Markdown with Gherkin flavor requires style=\"fenced\" \
1357 (a Gherkin Doc String is only ever a backtick fence). \
1358 Overriding style=\"indented\" to style=\"fenced\"."
1359 );
1360 assert_eq!(
1361 FlavorOverrideNotice::message(
1362 "MD048",
1363 "style",
1364 "tilde",
1365 "backtick",
1366 "a Gherkin Doc String is only ever a backtick fence"
1367 ),
1368 "\x1b[33m[config warning]\x1b[0m MD048: Markdown with Gherkin flavor requires style=\"backtick\" \
1369 (a Gherkin Doc String is only ever a backtick fence). \
1370 Overriding style=\"tilde\" to style=\"backtick\"."
1371 );
1372 assert_eq!(
1373 FlavorOverrideNotice::message(
1374 "MD055",
1375 "style",
1376 "no_leading_or_trailing",
1377 "leading_and_trailing",
1378 "a Gherkin table row is an indent followed directly by a pipe"
1379 ),
1380 "\x1b[33m[config warning]\x1b[0m MD055: Markdown with Gherkin flavor requires \
1381 style=\"leading_and_trailing\" \
1382 (a Gherkin table row is an indent followed directly by a pipe). \
1383 Overriding style=\"no_leading_or_trailing\" to style=\"leading_and_trailing\"."
1384 );
1385 }
1386
1387 #[test]
1391 fn test_end_to_end_md013_line_length_config() {
1392 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
1394
1395 let mut config = crate::config::Config::default();
1397 let json = serde_json::json!({
1398 "line-length": 40
1399 });
1400 if let Some(rule_config) = json_to_rule_config(&json) {
1401 config.rules.insert("MD013".to_string(), rule_config);
1402 }
1403
1404 config.global.enable = vec!["MD013".to_string()];
1406
1407 let rules = crate::rules::all_rules(&config);
1408 let filtered = crate::rules::filter_rules(&rules, &config.global);
1409
1410 let result = crate::lint(
1411 content,
1412 &filtered,
1413 false,
1414 crate::config::MarkdownFlavor::Standard,
1415 None,
1416 Some(&config),
1417 );
1418
1419 let warnings = result.expect("Linting should succeed");
1420
1421 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
1423 assert!(has_md013, "Should have MD013 warning with line-length=40");
1424 }
1425
1426 #[test]
1427 fn test_end_to_end_md013_line_length_no_warning() {
1428 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
1430
1431 let mut config = crate::config::Config::default();
1433 let json = serde_json::json!({
1434 "line-length": 100
1435 });
1436 if let Some(rule_config) = json_to_rule_config(&json) {
1437 config.rules.insert("MD013".to_string(), rule_config);
1438 }
1439
1440 config.global.enable = vec!["MD013".to_string()];
1442
1443 let rules = crate::rules::all_rules(&config);
1444 let filtered = crate::rules::filter_rules(&rules, &config.global);
1445
1446 let result = crate::lint(
1447 content,
1448 &filtered,
1449 false,
1450 crate::config::MarkdownFlavor::Standard,
1451 None,
1452 Some(&config),
1453 );
1454
1455 let warnings = result.expect("Linting should succeed");
1456
1457 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
1459 assert!(!has_md013, "Should NOT have MD013 warning with line-length=100");
1460 }
1461
1462 #[test]
1463 fn test_end_to_end_md044_proper_names() {
1464 let content = "# Test\n\nWe use javascript and typescript.\n";
1466
1467 let mut config = crate::config::Config::default();
1469 let json = serde_json::json!({
1470 "names": ["JavaScript", "TypeScript"],
1471 "code-blocks": false
1472 });
1473 if let Some(rule_config) = json_to_rule_config(&json) {
1474 config.rules.insert("MD044".to_string(), rule_config);
1475 }
1476
1477 config.global.enable = vec!["MD044".to_string()];
1479
1480 let rules = crate::rules::all_rules(&config);
1481 let filtered = crate::rules::filter_rules(&rules, &config.global);
1482
1483 let result = crate::lint(
1484 content,
1485 &filtered,
1486 false,
1487 crate::config::MarkdownFlavor::Standard,
1488 None,
1489 Some(&config),
1490 );
1491
1492 let warnings = result.expect("Linting should succeed");
1493
1494 let md044_warnings: Vec<_> = warnings
1496 .iter()
1497 .filter(|w| w.rule_name.as_deref() == Some("MD044"))
1498 .collect();
1499
1500 assert!(
1501 md044_warnings.len() >= 2,
1502 "Should have MD044 warnings for 'javascript' and 'typescript', got {}",
1503 md044_warnings.len()
1504 );
1505 }
1506
1507 #[test]
1508 fn test_end_to_end_severity_config() {
1509 let content = "test\n"; let mut config = crate::config::Config::default();
1513 let json = serde_json::json!({
1514 "severity": "info"
1515 });
1516 if let Some(rule_config) = json_to_rule_config(&json) {
1517 config.rules.insert("MD041".to_string(), rule_config);
1518 }
1519
1520 config.global.enable = vec!["MD041".to_string()];
1522
1523 let rules = crate::rules::all_rules(&config);
1524 let filtered = crate::rules::filter_rules(&rules, &config.global);
1525
1526 let result = crate::lint(
1527 content,
1528 &filtered,
1529 false,
1530 crate::config::MarkdownFlavor::Standard,
1531 None,
1532 Some(&config),
1533 );
1534
1535 let warnings = result.expect("Linting should succeed");
1536
1537 let md041 = warnings.iter().find(|w| w.rule_name.as_deref() == Some("MD041"));
1539 assert!(md041.is_some(), "Should have MD041 warning");
1540 assert_eq!(
1541 md041.unwrap().severity,
1542 crate::rule::Severity::Info,
1543 "MD041 should have Info severity from config"
1544 );
1545 }
1546}