1use std::collections::BTreeMap;
2use std::marker::PhantomData;
3use std::path::Path;
4use std::sync::{Arc, OnceLock};
5
6use super::flavor::ConfigLoaded;
7use super::flavor::ConfigValidated;
8use super::parsers;
9use super::registry::RuleRegistry;
10use super::source_tracking::{
11 ConfigSource, ConfigValidationWarning, SourcedConfig, SourcedConfigFragment, SourcedGlobalConfig, SourcedValue,
12};
13use super::types::{Config, ConfigError, GlobalConfig, MARKDOWNLINT_CONFIG_FILES, RuleConfig};
14use super::validation::validate_config_sourced_internal;
15
16impl SourcedConfig<ConfigLoaded> {
17 pub(super) fn merge(&mut self, fragment: SourcedConfigFragment) {
20 self.global.enable.merge_override(
23 fragment.global.enable.value,
24 fragment.global.enable.source,
25 fragment.global.enable.overrides.first().and_then(|o| o.file.clone()),
26 fragment.global.enable.overrides.first().and_then(|o| o.line),
27 );
28
29 self.global.disable.merge_union(
31 fragment.global.disable.value,
32 fragment.global.disable.source,
33 fragment.global.disable.overrides.first().and_then(|o| o.file.clone()),
34 fragment.global.disable.overrides.first().and_then(|o| o.line),
35 );
36
37 self.global.extend_enable.merge_union(
39 fragment.global.extend_enable.value,
40 fragment.global.extend_enable.source,
41 fragment
42 .global
43 .extend_enable
44 .overrides
45 .first()
46 .and_then(|o| o.file.clone()),
47 fragment.global.extend_enable.overrides.first().and_then(|o| o.line),
48 );
49
50 self.global.extend_disable.merge_union(
52 fragment.global.extend_disable.value,
53 fragment.global.extend_disable.source,
54 fragment
55 .global
56 .extend_disable
57 .overrides
58 .first()
59 .and_then(|o| o.file.clone()),
60 fragment.global.extend_disable.overrides.first().and_then(|o| o.line),
61 );
62
63 self.global
66 .disable
67 .value
68 .retain(|rule| !self.global.enable.value.contains(rule));
69 self.global.include.merge_override(
70 fragment.global.include.value,
71 fragment.global.include.source,
72 fragment.global.include.overrides.first().and_then(|o| o.file.clone()),
73 fragment.global.include.overrides.first().and_then(|o| o.line),
74 );
75 self.global.exclude.merge_override(
76 fragment.global.exclude.value,
77 fragment.global.exclude.source,
78 fragment.global.exclude.overrides.first().and_then(|o| o.file.clone()),
79 fragment.global.exclude.overrides.first().and_then(|o| o.line),
80 );
81 self.global.respect_gitignore.merge_override(
82 fragment.global.respect_gitignore.value,
83 fragment.global.respect_gitignore.source,
84 fragment
85 .global
86 .respect_gitignore
87 .overrides
88 .first()
89 .and_then(|o| o.file.clone()),
90 fragment.global.respect_gitignore.overrides.first().and_then(|o| o.line),
91 );
92 self.global.line_length.merge_override(
93 fragment.global.line_length.value,
94 fragment.global.line_length.source,
95 fragment
96 .global
97 .line_length
98 .overrides
99 .first()
100 .and_then(|o| o.file.clone()),
101 fragment.global.line_length.overrides.first().and_then(|o| o.line),
102 );
103 self.global.fixable.merge_override(
104 fragment.global.fixable.value,
105 fragment.global.fixable.source,
106 fragment.global.fixable.overrides.first().and_then(|o| o.file.clone()),
107 fragment.global.fixable.overrides.first().and_then(|o| o.line),
108 );
109 self.global.unfixable.merge_override(
110 fragment.global.unfixable.value,
111 fragment.global.unfixable.source,
112 fragment.global.unfixable.overrides.first().and_then(|o| o.file.clone()),
113 fragment.global.unfixable.overrides.first().and_then(|o| o.line),
114 );
115
116 self.global.flavor.merge_override(
118 fragment.global.flavor.value,
119 fragment.global.flavor.source,
120 fragment.global.flavor.overrides.first().and_then(|o| o.file.clone()),
121 fragment.global.flavor.overrides.first().and_then(|o| o.line),
122 );
123
124 self.global.force_exclude.merge_override(
126 fragment.global.force_exclude.value,
127 fragment.global.force_exclude.source,
128 fragment
129 .global
130 .force_exclude
131 .overrides
132 .first()
133 .and_then(|o| o.file.clone()),
134 fragment.global.force_exclude.overrides.first().and_then(|o| o.line),
135 );
136
137 if let Some(output_format_fragment) = fragment.global.output_format {
139 if let Some(ref mut output_format) = self.global.output_format {
140 output_format.merge_override(
141 output_format_fragment.value,
142 output_format_fragment.source,
143 output_format_fragment.overrides.first().and_then(|o| o.file.clone()),
144 output_format_fragment.overrides.first().and_then(|o| o.line),
145 );
146 } else {
147 self.global.output_format = Some(output_format_fragment);
148 }
149 }
150
151 if let Some(cache_dir_fragment) = fragment.global.cache_dir {
153 if let Some(ref mut cache_dir) = self.global.cache_dir {
154 cache_dir.merge_override(
155 cache_dir_fragment.value,
156 cache_dir_fragment.source,
157 cache_dir_fragment.overrides.first().and_then(|o| o.file.clone()),
158 cache_dir_fragment.overrides.first().and_then(|o| o.line),
159 );
160 } else {
161 self.global.cache_dir = Some(cache_dir_fragment);
162 }
163 }
164
165 if fragment.global.cache.source != ConfigSource::Default {
167 self.global.cache.merge_override(
168 fragment.global.cache.value,
169 fragment.global.cache.source,
170 fragment.global.cache.overrides.first().and_then(|o| o.file.clone()),
171 fragment.global.cache.overrides.first().and_then(|o| o.line),
172 );
173 }
174
175 self.per_file_ignores.merge_override(
177 fragment.per_file_ignores.value,
178 fragment.per_file_ignores.source,
179 fragment.per_file_ignores.overrides.first().and_then(|o| o.file.clone()),
180 fragment.per_file_ignores.overrides.first().and_then(|o| o.line),
181 );
182
183 self.per_file_flavor.merge_override(
185 fragment.per_file_flavor.value,
186 fragment.per_file_flavor.source,
187 fragment.per_file_flavor.overrides.first().and_then(|o| o.file.clone()),
188 fragment.per_file_flavor.overrides.first().and_then(|o| o.line),
189 );
190
191 self.code_block_tools.merge_override(
193 fragment.code_block_tools.value,
194 fragment.code_block_tools.source,
195 fragment.code_block_tools.overrides.first().and_then(|o| o.file.clone()),
196 fragment.code_block_tools.overrides.first().and_then(|o| o.line),
197 );
198
199 for (rule_name, rule_fragment) in fragment.rules {
201 let norm_rule_name = rule_name.to_ascii_uppercase(); let rule_entry = self.rules.entry(norm_rule_name).or_default();
203
204 if let Some(severity_fragment) = rule_fragment.severity {
206 if let Some(ref mut existing_severity) = rule_entry.severity {
207 existing_severity.merge_override(
208 severity_fragment.value,
209 severity_fragment.source,
210 severity_fragment.overrides.first().and_then(|o| o.file.clone()),
211 severity_fragment.overrides.first().and_then(|o| o.line),
212 );
213 } else {
214 rule_entry.severity = Some(severity_fragment);
215 }
216 }
217
218 for (key, sourced_value_fragment) in rule_fragment.values {
220 let sv_entry = rule_entry
221 .values
222 .entry(key.clone())
223 .or_insert_with(|| SourcedValue::new(sourced_value_fragment.value.clone(), ConfigSource::Default));
224 let file_from_fragment = sourced_value_fragment.overrides.first().and_then(|o| o.file.clone());
225 let line_from_fragment = sourced_value_fragment.overrides.first().and_then(|o| o.line);
226 sv_entry.merge_override(
227 sourced_value_fragment.value, sourced_value_fragment.source, file_from_fragment, line_from_fragment, );
232 }
233 }
234
235 for (section, key, file_path) in fragment.unknown_keys {
237 if !self.unknown_keys.iter().any(|(s, k, _)| s == §ion && k == &key) {
239 self.unknown_keys.push((section, key, file_path));
240 }
241 }
242 }
243
244 pub fn load(config_path: Option<&str>, cli_overrides: Option<&SourcedGlobalConfig>) -> Result<Self, ConfigError> {
246 Self::load_with_discovery(config_path, cli_overrides, false)
247 }
248
249 fn find_project_root_from(start_dir: &Path) -> std::path::PathBuf {
252 let mut current = if start_dir.is_relative() {
254 std::env::current_dir()
255 .map(|cwd| cwd.join(start_dir))
256 .unwrap_or_else(|_| start_dir.to_path_buf())
257 } else {
258 start_dir.to_path_buf()
259 };
260 const MAX_DEPTH: usize = 100;
261
262 for _ in 0..MAX_DEPTH {
263 if current.join(".git").exists() {
264 log::debug!("[rumdl-config] Found .git at: {}", current.display());
265 return current;
266 }
267
268 match current.parent() {
269 Some(parent) => current = parent.to_path_buf(),
270 None => break,
271 }
272 }
273
274 log::debug!(
276 "[rumdl-config] No .git found, using config location as project root: {}",
277 start_dir.display()
278 );
279 start_dir.to_path_buf()
280 }
281
282 fn discover_config_upward() -> Option<(std::path::PathBuf, std::path::PathBuf)> {
288 use std::env;
289
290 const CONFIG_FILES: &[&str] = &[".rumdl.toml", "rumdl.toml", ".config/rumdl.toml", "pyproject.toml"];
291 const MAX_DEPTH: usize = 100; let start_dir = match env::current_dir() {
294 Ok(dir) => dir,
295 Err(e) => {
296 log::debug!("[rumdl-config] Failed to get current directory: {e}");
297 return None;
298 }
299 };
300
301 let mut current_dir = start_dir.clone();
302 let mut depth = 0;
303 let mut found_config: Option<(std::path::PathBuf, std::path::PathBuf)> = None;
304
305 loop {
306 if depth >= MAX_DEPTH {
307 log::debug!("[rumdl-config] Maximum traversal depth reached");
308 break;
309 }
310
311 log::debug!("[rumdl-config] Searching for config in: {}", current_dir.display());
312
313 if found_config.is_none() {
315 for config_name in CONFIG_FILES {
316 let config_path = current_dir.join(config_name);
317
318 if config_path.exists() {
319 if *config_name == "pyproject.toml" {
321 if let Ok(content) = std::fs::read_to_string(&config_path) {
322 if content.contains("[tool.rumdl]") || content.contains("tool.rumdl") {
323 log::debug!("[rumdl-config] Found config file: {}", config_path.display());
324 found_config = Some((config_path.clone(), current_dir.clone()));
326 break;
327 }
328 log::debug!("[rumdl-config] Found pyproject.toml but no [tool.rumdl] section");
329 continue;
330 }
331 } else {
332 log::debug!("[rumdl-config] Found config file: {}", config_path.display());
333 found_config = Some((config_path.clone(), current_dir.clone()));
335 break;
336 }
337 }
338 }
339 }
340
341 if current_dir.join(".git").exists() {
343 log::debug!("[rumdl-config] Stopping at .git directory");
344 break;
345 }
346
347 match current_dir.parent() {
349 Some(parent) => {
350 current_dir = parent.to_owned();
351 depth += 1;
352 }
353 None => {
354 log::debug!("[rumdl-config] Reached filesystem root");
355 break;
356 }
357 }
358 }
359
360 if let Some((config_path, config_dir)) = found_config {
362 let project_root = Self::find_project_root_from(&config_dir);
363 return Some((config_path, project_root));
364 }
365
366 None
367 }
368
369 fn discover_markdownlint_config_upward() -> Option<std::path::PathBuf> {
373 use std::env;
374
375 const MAX_DEPTH: usize = 100;
376
377 let start_dir = match env::current_dir() {
378 Ok(dir) => dir,
379 Err(e) => {
380 log::debug!("[rumdl-config] Failed to get current directory for markdownlint discovery: {e}");
381 return None;
382 }
383 };
384
385 let mut current_dir = start_dir.clone();
386 let mut depth = 0;
387
388 loop {
389 if depth >= MAX_DEPTH {
390 log::debug!("[rumdl-config] Maximum traversal depth reached for markdownlint discovery");
391 break;
392 }
393
394 log::debug!(
395 "[rumdl-config] Searching for markdownlint config in: {}",
396 current_dir.display()
397 );
398
399 for config_name in MARKDOWNLINT_CONFIG_FILES {
401 let config_path = current_dir.join(config_name);
402 if config_path.exists() {
403 log::debug!("[rumdl-config] Found markdownlint config: {}", config_path.display());
404 return Some(config_path);
405 }
406 }
407
408 if current_dir.join(".git").exists() {
410 log::debug!("[rumdl-config] Stopping markdownlint search at .git directory");
411 break;
412 }
413
414 match current_dir.parent() {
416 Some(parent) => {
417 current_dir = parent.to_owned();
418 depth += 1;
419 }
420 None => {
421 log::debug!("[rumdl-config] Reached filesystem root during markdownlint search");
422 break;
423 }
424 }
425 }
426
427 None
428 }
429
430 fn user_configuration_path_impl(config_dir: &Path) -> Option<std::path::PathBuf> {
432 let config_dir = config_dir.join("rumdl");
433
434 const USER_CONFIG_FILES: &[&str] = &[".rumdl.toml", "rumdl.toml", "pyproject.toml"];
436
437 log::debug!(
438 "[rumdl-config] Checking for user configuration in: {}",
439 config_dir.display()
440 );
441
442 for filename in USER_CONFIG_FILES {
443 let config_path = config_dir.join(filename);
444
445 if config_path.exists() {
446 if *filename == "pyproject.toml" {
448 if let Ok(content) = std::fs::read_to_string(&config_path) {
449 if content.contains("[tool.rumdl]") || content.contains("tool.rumdl") {
450 log::debug!("[rumdl-config] Found user configuration at: {}", config_path.display());
451 return Some(config_path);
452 }
453 log::debug!("[rumdl-config] Found user pyproject.toml but no [tool.rumdl] section");
454 continue;
455 }
456 } else {
457 log::debug!("[rumdl-config] Found user configuration at: {}", config_path.display());
458 return Some(config_path);
459 }
460 }
461 }
462
463 log::debug!(
464 "[rumdl-config] No user configuration found in: {}",
465 config_dir.display()
466 );
467 None
468 }
469
470 #[cfg(feature = "native")]
473 fn user_configuration_path() -> Option<std::path::PathBuf> {
474 use etcetera::{BaseStrategy, choose_base_strategy};
475
476 match choose_base_strategy() {
477 Ok(strategy) => {
478 let config_dir = strategy.config_dir();
479 Self::user_configuration_path_impl(&config_dir)
480 }
481 Err(e) => {
482 log::debug!("[rumdl-config] Failed to determine user config directory: {e}");
483 None
484 }
485 }
486 }
487
488 #[cfg(not(feature = "native"))]
490 fn user_configuration_path() -> Option<std::path::PathBuf> {
491 None
492 }
493
494 fn load_explicit_config(sourced_config: &mut Self, path: &str) -> Result<(), ConfigError> {
496 let path_obj = Path::new(path);
497 let filename = path_obj.file_name().and_then(|name| name.to_str()).unwrap_or("");
498 let path_str = path.to_string();
499
500 log::debug!("[rumdl-config] Loading explicit config file: {filename}");
501
502 if let Some(config_parent) = path_obj.parent() {
504 let project_root = Self::find_project_root_from(config_parent);
505 log::debug!(
506 "[rumdl-config] Project root (from explicit config): {}",
507 project_root.display()
508 );
509 sourced_config.project_root = Some(project_root);
510 }
511
512 const MARKDOWNLINT_FILENAMES: &[&str] = &[".markdownlint.json", ".markdownlint.yaml", ".markdownlint.yml"];
514
515 if filename == "pyproject.toml" || filename == ".rumdl.toml" || filename == "rumdl.toml" {
516 let content = std::fs::read_to_string(path).map_err(|e| ConfigError::IoError {
517 source: e,
518 path: path_str.clone(),
519 })?;
520 if filename == "pyproject.toml" {
521 if let Some(fragment) = parsers::parse_pyproject_toml(&content, &path_str)? {
522 sourced_config.merge(fragment);
523 sourced_config.loaded_files.push(path_str);
524 }
525 } else {
526 let fragment = parsers::parse_rumdl_toml(&content, &path_str, ConfigSource::ProjectConfig)?;
527 sourced_config.merge(fragment);
528 sourced_config.loaded_files.push(path_str);
529 }
530 } else if MARKDOWNLINT_FILENAMES.contains(&filename)
531 || path_str.ends_with(".json")
532 || path_str.ends_with(".jsonc")
533 || path_str.ends_with(".yaml")
534 || path_str.ends_with(".yml")
535 {
536 let fragment = parsers::load_from_markdownlint(&path_str)?;
538 sourced_config.merge(fragment);
539 sourced_config.loaded_files.push(path_str);
540 } else {
541 let content = std::fs::read_to_string(path).map_err(|e| ConfigError::IoError {
543 source: e,
544 path: path_str.clone(),
545 })?;
546 let fragment = parsers::parse_rumdl_toml(&content, &path_str, ConfigSource::ProjectConfig)?;
547 sourced_config.merge(fragment);
548 sourced_config.loaded_files.push(path_str);
549 }
550
551 Ok(())
552 }
553
554 fn load_user_config_as_fallback(
556 sourced_config: &mut Self,
557 user_config_dir: Option<&Path>,
558 ) -> Result<(), ConfigError> {
559 let user_config_path = if let Some(dir) = user_config_dir {
560 Self::user_configuration_path_impl(dir)
561 } else {
562 Self::user_configuration_path()
563 };
564
565 if let Some(user_config_path) = user_config_path {
566 let path_str = user_config_path.display().to_string();
567 let filename = user_config_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
568
569 log::debug!("[rumdl-config] Loading user config as fallback: {path_str}");
570
571 if filename == "pyproject.toml" {
572 let content = std::fs::read_to_string(&user_config_path).map_err(|e| ConfigError::IoError {
573 source: e,
574 path: path_str.clone(),
575 })?;
576 if let Some(fragment) = parsers::parse_pyproject_toml(&content, &path_str)? {
577 sourced_config.merge(fragment);
578 sourced_config.loaded_files.push(path_str);
579 }
580 } else {
581 let content = std::fs::read_to_string(&user_config_path).map_err(|e| ConfigError::IoError {
582 source: e,
583 path: path_str.clone(),
584 })?;
585 let fragment = parsers::parse_rumdl_toml(&content, &path_str, ConfigSource::UserConfig)?;
586 sourced_config.merge(fragment);
587 sourced_config.loaded_files.push(path_str);
588 }
589 } else {
590 log::debug!("[rumdl-config] No user configuration file found");
591 }
592
593 Ok(())
594 }
595
596 #[doc(hidden)]
598 pub fn load_with_discovery_impl(
599 config_path: Option<&str>,
600 cli_overrides: Option<&SourcedGlobalConfig>,
601 skip_auto_discovery: bool,
602 user_config_dir: Option<&Path>,
603 ) -> Result<Self, ConfigError> {
604 use std::env;
605 log::debug!("[rumdl-config] Current working directory: {:?}", env::current_dir());
606
607 let mut sourced_config = SourcedConfig::default();
608
609 if let Some(path) = config_path {
622 log::debug!("[rumdl-config] Explicit config_path provided: {path:?}");
624 Self::load_explicit_config(&mut sourced_config, path)?;
625 } else if skip_auto_discovery {
626 log::debug!("[rumdl-config] Skipping config discovery due to --no-config/--isolated flag");
627 } else {
629 log::debug!("[rumdl-config] No explicit config_path, searching default locations");
631
632 if let Some((config_file, project_root)) = Self::discover_config_upward() {
634 let path_str = config_file.display().to_string();
636 let filename = config_file.file_name().and_then(|n| n.to_str()).unwrap_or("");
637
638 log::debug!("[rumdl-config] Found project config: {path_str}");
639 log::debug!("[rumdl-config] Project root: {}", project_root.display());
640
641 sourced_config.project_root = Some(project_root);
642
643 if filename == "pyproject.toml" {
644 let content = std::fs::read_to_string(&config_file).map_err(|e| ConfigError::IoError {
645 source: e,
646 path: path_str.clone(),
647 })?;
648 if let Some(fragment) = parsers::parse_pyproject_toml(&content, &path_str)? {
649 sourced_config.merge(fragment);
650 sourced_config.loaded_files.push(path_str);
651 }
652 } else if filename == ".rumdl.toml" || filename == "rumdl.toml" {
653 let content = std::fs::read_to_string(&config_file).map_err(|e| ConfigError::IoError {
654 source: e,
655 path: path_str.clone(),
656 })?;
657 let fragment = parsers::parse_rumdl_toml(&content, &path_str, ConfigSource::ProjectConfig)?;
658 sourced_config.merge(fragment);
659 sourced_config.loaded_files.push(path_str);
660 }
661 } else {
662 log::debug!("[rumdl-config] No rumdl config found, checking markdownlint config");
664
665 if let Some(markdownlint_path) = Self::discover_markdownlint_config_upward() {
666 let path_str = markdownlint_path.display().to_string();
667 log::debug!("[rumdl-config] Found markdownlint config: {path_str}");
668 match parsers::load_from_markdownlint(&path_str) {
669 Ok(fragment) => {
670 sourced_config.merge(fragment);
671 sourced_config.loaded_files.push(path_str);
672 }
673 Err(_e) => {
674 log::debug!("[rumdl-config] Failed to load markdownlint config, trying user config");
675 Self::load_user_config_as_fallback(&mut sourced_config, user_config_dir)?;
676 }
677 }
678 } else {
679 log::debug!("[rumdl-config] No project config found, using user config as fallback");
681 Self::load_user_config_as_fallback(&mut sourced_config, user_config_dir)?;
682 }
683 }
684 }
685
686 if let Some(cli) = cli_overrides {
688 sourced_config
689 .global
690 .enable
691 .merge_override(cli.enable.value.clone(), ConfigSource::Cli, None, None);
692 sourced_config
693 .global
694 .disable
695 .merge_override(cli.disable.value.clone(), ConfigSource::Cli, None, None);
696 sourced_config
697 .global
698 .exclude
699 .merge_override(cli.exclude.value.clone(), ConfigSource::Cli, None, None);
700 sourced_config
701 .global
702 .include
703 .merge_override(cli.include.value.clone(), ConfigSource::Cli, None, None);
704 sourced_config.global.respect_gitignore.merge_override(
705 cli.respect_gitignore.value,
706 ConfigSource::Cli,
707 None,
708 None,
709 );
710 sourced_config
711 .global
712 .fixable
713 .merge_override(cli.fixable.value.clone(), ConfigSource::Cli, None, None);
714 sourced_config
715 .global
716 .unfixable
717 .merge_override(cli.unfixable.value.clone(), ConfigSource::Cli, None, None);
718 }
720
721 Ok(sourced_config)
724 }
725
726 pub fn load_with_discovery(
729 config_path: Option<&str>,
730 cli_overrides: Option<&SourcedGlobalConfig>,
731 skip_auto_discovery: bool,
732 ) -> Result<Self, ConfigError> {
733 Self::load_with_discovery_impl(config_path, cli_overrides, skip_auto_discovery, None)
734 }
735
736 pub fn validate(self, registry: &RuleRegistry) -> Result<SourcedConfig<ConfigValidated>, ConfigError> {
750 let warnings = validate_config_sourced_internal(&self, registry);
751
752 Ok(SourcedConfig {
753 global: self.global,
754 per_file_ignores: self.per_file_ignores,
755 per_file_flavor: self.per_file_flavor,
756 code_block_tools: self.code_block_tools,
757 rules: self.rules,
758 loaded_files: self.loaded_files,
759 unknown_keys: self.unknown_keys,
760 project_root: self.project_root,
761 validation_warnings: warnings,
762 _state: PhantomData,
763 })
764 }
765
766 pub fn validate_into(self, registry: &RuleRegistry) -> Result<(Config, Vec<ConfigValidationWarning>), ConfigError> {
771 let validated = self.validate(registry)?;
772 let warnings = validated.validation_warnings.clone();
773 Ok((validated.into(), warnings))
774 }
775
776 pub fn into_validated_unchecked(self) -> SourcedConfig<ConfigValidated> {
787 SourcedConfig {
788 global: self.global,
789 per_file_ignores: self.per_file_ignores,
790 per_file_flavor: self.per_file_flavor,
791 code_block_tools: self.code_block_tools,
792 rules: self.rules,
793 loaded_files: self.loaded_files,
794 unknown_keys: self.unknown_keys,
795 project_root: self.project_root,
796 validation_warnings: Vec::new(),
797 _state: PhantomData,
798 }
799 }
800}
801
802impl From<SourcedConfig<ConfigValidated>> for Config {
807 fn from(sourced: SourcedConfig<ConfigValidated>) -> Self {
808 let mut rules = BTreeMap::new();
809 for (rule_name, sourced_rule_cfg) in sourced.rules {
810 let normalized_rule_name = rule_name.to_ascii_uppercase();
812 let severity = sourced_rule_cfg.severity.map(|sv| sv.value);
813 let mut values = BTreeMap::new();
814 for (key, sourced_val) in sourced_rule_cfg.values {
815 values.insert(key, sourced_val.value);
816 }
817 rules.insert(normalized_rule_name, RuleConfig { severity, values });
818 }
819 let enable_is_explicit = sourced.global.enable.source != ConfigSource::Default;
821
822 #[allow(deprecated)]
823 let global = GlobalConfig {
824 enable: sourced.global.enable.value,
825 disable: sourced.global.disable.value,
826 exclude: sourced.global.exclude.value,
827 include: sourced.global.include.value,
828 respect_gitignore: sourced.global.respect_gitignore.value,
829 line_length: sourced.global.line_length.value,
830 output_format: sourced.global.output_format.as_ref().map(|v| v.value.clone()),
831 fixable: sourced.global.fixable.value,
832 unfixable: sourced.global.unfixable.value,
833 flavor: sourced.global.flavor.value,
834 force_exclude: sourced.global.force_exclude.value,
835 cache_dir: sourced.global.cache_dir.as_ref().map(|v| v.value.clone()),
836 cache: sourced.global.cache.value,
837 extend_enable: sourced.global.extend_enable.value,
838 extend_disable: sourced.global.extend_disable.value,
839 enable_is_explicit,
840 };
841
842 let opt_in_with_enabled = ["MD060", "MD063", "MD072", "MD073"];
846 let mut global = global;
847 for rule_name in opt_in_with_enabled {
848 if let Some(rule_cfg) = rules.get(rule_name)
849 && let Some(toml::Value::Boolean(true)) = rule_cfg.values.get("enabled")
850 && !global.extend_enable.contains(&rule_name.to_string())
851 {
852 log::warn!(
853 "[DEPRECATED] [{rule_name}] enabled = true is deprecated. \
854 Use `extend-enable = [\"{rule_name}\"]` in [global] instead.",
855 );
856 global.extend_enable.push(rule_name.to_string());
857 }
858 }
859
860 Config {
861 global,
862 per_file_ignores: sourced.per_file_ignores.value,
863 per_file_flavor: sourced.per_file_flavor.value,
864 code_block_tools: sourced.code_block_tools.value,
865 rules,
866 project_root: sourced.project_root,
867 per_file_ignores_cache: Arc::new(OnceLock::new()),
868 per_file_flavor_cache: Arc::new(OnceLock::new()),
869 }
870 }
871}