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