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
40 .disable
41 .value
42 .retain(|rule| !self.global.enable.value.contains(rule));
43 self.global.include.merge_override(
44 fragment.global.include.value,
45 fragment.global.include.source,
46 fragment.global.include.overrides.first().and_then(|o| o.file.clone()),
47 fragment.global.include.overrides.first().and_then(|o| o.line),
48 );
49 self.global.exclude.merge_override(
50 fragment.global.exclude.value,
51 fragment.global.exclude.source,
52 fragment.global.exclude.overrides.first().and_then(|o| o.file.clone()),
53 fragment.global.exclude.overrides.first().and_then(|o| o.line),
54 );
55 self.global.respect_gitignore.merge_override(
56 fragment.global.respect_gitignore.value,
57 fragment.global.respect_gitignore.source,
58 fragment
59 .global
60 .respect_gitignore
61 .overrides
62 .first()
63 .and_then(|o| o.file.clone()),
64 fragment.global.respect_gitignore.overrides.first().and_then(|o| o.line),
65 );
66 self.global.line_length.merge_override(
67 fragment.global.line_length.value,
68 fragment.global.line_length.source,
69 fragment
70 .global
71 .line_length
72 .overrides
73 .first()
74 .and_then(|o| o.file.clone()),
75 fragment.global.line_length.overrides.first().and_then(|o| o.line),
76 );
77 self.global.fixable.merge_override(
78 fragment.global.fixable.value,
79 fragment.global.fixable.source,
80 fragment.global.fixable.overrides.first().and_then(|o| o.file.clone()),
81 fragment.global.fixable.overrides.first().and_then(|o| o.line),
82 );
83 self.global.unfixable.merge_override(
84 fragment.global.unfixable.value,
85 fragment.global.unfixable.source,
86 fragment.global.unfixable.overrides.first().and_then(|o| o.file.clone()),
87 fragment.global.unfixable.overrides.first().and_then(|o| o.line),
88 );
89
90 self.global.flavor.merge_override(
92 fragment.global.flavor.value,
93 fragment.global.flavor.source,
94 fragment.global.flavor.overrides.first().and_then(|o| o.file.clone()),
95 fragment.global.flavor.overrides.first().and_then(|o| o.line),
96 );
97
98 self.global.force_exclude.merge_override(
100 fragment.global.force_exclude.value,
101 fragment.global.force_exclude.source,
102 fragment
103 .global
104 .force_exclude
105 .overrides
106 .first()
107 .and_then(|o| o.file.clone()),
108 fragment.global.force_exclude.overrides.first().and_then(|o| o.line),
109 );
110
111 if let Some(output_format_fragment) = fragment.global.output_format {
113 if let Some(ref mut output_format) = self.global.output_format {
114 output_format.merge_override(
115 output_format_fragment.value,
116 output_format_fragment.source,
117 output_format_fragment.overrides.first().and_then(|o| o.file.clone()),
118 output_format_fragment.overrides.first().and_then(|o| o.line),
119 );
120 } else {
121 self.global.output_format = Some(output_format_fragment);
122 }
123 }
124
125 if let Some(cache_dir_fragment) = fragment.global.cache_dir {
127 if let Some(ref mut cache_dir) = self.global.cache_dir {
128 cache_dir.merge_override(
129 cache_dir_fragment.value,
130 cache_dir_fragment.source,
131 cache_dir_fragment.overrides.first().and_then(|o| o.file.clone()),
132 cache_dir_fragment.overrides.first().and_then(|o| o.line),
133 );
134 } else {
135 self.global.cache_dir = Some(cache_dir_fragment);
136 }
137 }
138
139 if fragment.global.cache.source != ConfigSource::Default {
141 self.global.cache.merge_override(
142 fragment.global.cache.value,
143 fragment.global.cache.source,
144 fragment.global.cache.overrides.first().and_then(|o| o.file.clone()),
145 fragment.global.cache.overrides.first().and_then(|o| o.line),
146 );
147 }
148
149 self.per_file_ignores.merge_override(
151 fragment.per_file_ignores.value,
152 fragment.per_file_ignores.source,
153 fragment.per_file_ignores.overrides.first().and_then(|o| o.file.clone()),
154 fragment.per_file_ignores.overrides.first().and_then(|o| o.line),
155 );
156
157 self.per_file_flavor.merge_override(
159 fragment.per_file_flavor.value,
160 fragment.per_file_flavor.source,
161 fragment.per_file_flavor.overrides.first().and_then(|o| o.file.clone()),
162 fragment.per_file_flavor.overrides.first().and_then(|o| o.line),
163 );
164
165 self.code_block_tools.merge_override(
167 fragment.code_block_tools.value,
168 fragment.code_block_tools.source,
169 fragment.code_block_tools.overrides.first().and_then(|o| o.file.clone()),
170 fragment.code_block_tools.overrides.first().and_then(|o| o.line),
171 );
172
173 for (rule_name, rule_fragment) in fragment.rules {
175 let norm_rule_name = rule_name.to_ascii_uppercase(); let rule_entry = self.rules.entry(norm_rule_name).or_default();
177
178 if let Some(severity_fragment) = rule_fragment.severity {
180 if let Some(ref mut existing_severity) = rule_entry.severity {
181 existing_severity.merge_override(
182 severity_fragment.value,
183 severity_fragment.source,
184 severity_fragment.overrides.first().and_then(|o| o.file.clone()),
185 severity_fragment.overrides.first().and_then(|o| o.line),
186 );
187 } else {
188 rule_entry.severity = Some(severity_fragment);
189 }
190 }
191
192 for (key, sourced_value_fragment) in rule_fragment.values {
194 let sv_entry = rule_entry
195 .values
196 .entry(key.clone())
197 .or_insert_with(|| SourcedValue::new(sourced_value_fragment.value.clone(), ConfigSource::Default));
198 let file_from_fragment = sourced_value_fragment.overrides.first().and_then(|o| o.file.clone());
199 let line_from_fragment = sourced_value_fragment.overrides.first().and_then(|o| o.line);
200 sv_entry.merge_override(
201 sourced_value_fragment.value, sourced_value_fragment.source, file_from_fragment, line_from_fragment, );
206 }
207 }
208
209 for (section, key, file_path) in fragment.unknown_keys {
211 if !self.unknown_keys.iter().any(|(s, k, _)| s == §ion && k == &key) {
213 self.unknown_keys.push((section, key, file_path));
214 }
215 }
216 }
217
218 pub fn load(config_path: Option<&str>, cli_overrides: Option<&SourcedGlobalConfig>) -> Result<Self, ConfigError> {
220 Self::load_with_discovery(config_path, cli_overrides, false)
221 }
222
223 fn find_project_root_from(start_dir: &Path) -> std::path::PathBuf {
226 let mut current = if start_dir.is_relative() {
228 std::env::current_dir()
229 .map(|cwd| cwd.join(start_dir))
230 .unwrap_or_else(|_| start_dir.to_path_buf())
231 } else {
232 start_dir.to_path_buf()
233 };
234 const MAX_DEPTH: usize = 100;
235
236 for _ in 0..MAX_DEPTH {
237 if current.join(".git").exists() {
238 log::debug!("[rumdl-config] Found .git at: {}", current.display());
239 return current;
240 }
241
242 match current.parent() {
243 Some(parent) => current = parent.to_path_buf(),
244 None => break,
245 }
246 }
247
248 log::debug!(
250 "[rumdl-config] No .git found, using config location as project root: {}",
251 start_dir.display()
252 );
253 start_dir.to_path_buf()
254 }
255
256 fn discover_config_upward() -> Option<(std::path::PathBuf, std::path::PathBuf)> {
262 use std::env;
263
264 const CONFIG_FILES: &[&str] = &[".rumdl.toml", "rumdl.toml", ".config/rumdl.toml", "pyproject.toml"];
265 const MAX_DEPTH: usize = 100; let start_dir = match env::current_dir() {
268 Ok(dir) => dir,
269 Err(e) => {
270 log::debug!("[rumdl-config] Failed to get current directory: {e}");
271 return None;
272 }
273 };
274
275 let mut current_dir = start_dir.clone();
276 let mut depth = 0;
277 let mut found_config: Option<(std::path::PathBuf, std::path::PathBuf)> = None;
278
279 loop {
280 if depth >= MAX_DEPTH {
281 log::debug!("[rumdl-config] Maximum traversal depth reached");
282 break;
283 }
284
285 log::debug!("[rumdl-config] Searching for config in: {}", current_dir.display());
286
287 if found_config.is_none() {
289 for config_name in CONFIG_FILES {
290 let config_path = current_dir.join(config_name);
291
292 if config_path.exists() {
293 if *config_name == "pyproject.toml" {
295 if let Ok(content) = std::fs::read_to_string(&config_path) {
296 if content.contains("[tool.rumdl]") || content.contains("tool.rumdl") {
297 log::debug!("[rumdl-config] Found config file: {}", config_path.display());
298 found_config = Some((config_path.clone(), current_dir.clone()));
300 break;
301 }
302 log::debug!("[rumdl-config] Found pyproject.toml but no [tool.rumdl] section");
303 continue;
304 }
305 } else {
306 log::debug!("[rumdl-config] Found config file: {}", config_path.display());
307 found_config = Some((config_path.clone(), current_dir.clone()));
309 break;
310 }
311 }
312 }
313 }
314
315 if current_dir.join(".git").exists() {
317 log::debug!("[rumdl-config] Stopping at .git directory");
318 break;
319 }
320
321 match current_dir.parent() {
323 Some(parent) => {
324 current_dir = parent.to_owned();
325 depth += 1;
326 }
327 None => {
328 log::debug!("[rumdl-config] Reached filesystem root");
329 break;
330 }
331 }
332 }
333
334 if let Some((config_path, config_dir)) = found_config {
336 let project_root = Self::find_project_root_from(&config_dir);
337 return Some((config_path, project_root));
338 }
339
340 None
341 }
342
343 fn discover_markdownlint_config_upward() -> Option<std::path::PathBuf> {
347 use std::env;
348
349 const MAX_DEPTH: usize = 100;
350
351 let start_dir = match env::current_dir() {
352 Ok(dir) => dir,
353 Err(e) => {
354 log::debug!("[rumdl-config] Failed to get current directory for markdownlint discovery: {e}");
355 return None;
356 }
357 };
358
359 let mut current_dir = start_dir.clone();
360 let mut depth = 0;
361
362 loop {
363 if depth >= MAX_DEPTH {
364 log::debug!("[rumdl-config] Maximum traversal depth reached for markdownlint discovery");
365 break;
366 }
367
368 log::debug!(
369 "[rumdl-config] Searching for markdownlint config in: {}",
370 current_dir.display()
371 );
372
373 for config_name in MARKDOWNLINT_CONFIG_FILES {
375 let config_path = current_dir.join(config_name);
376 if config_path.exists() {
377 log::debug!("[rumdl-config] Found markdownlint config: {}", config_path.display());
378 return Some(config_path);
379 }
380 }
381
382 if current_dir.join(".git").exists() {
384 log::debug!("[rumdl-config] Stopping markdownlint search at .git directory");
385 break;
386 }
387
388 match current_dir.parent() {
390 Some(parent) => {
391 current_dir = parent.to_owned();
392 depth += 1;
393 }
394 None => {
395 log::debug!("[rumdl-config] Reached filesystem root during markdownlint search");
396 break;
397 }
398 }
399 }
400
401 None
402 }
403
404 fn user_configuration_path_impl(config_dir: &Path) -> Option<std::path::PathBuf> {
406 let config_dir = config_dir.join("rumdl");
407
408 const USER_CONFIG_FILES: &[&str] = &[".rumdl.toml", "rumdl.toml", "pyproject.toml"];
410
411 log::debug!(
412 "[rumdl-config] Checking for user configuration in: {}",
413 config_dir.display()
414 );
415
416 for filename in USER_CONFIG_FILES {
417 let config_path = config_dir.join(filename);
418
419 if config_path.exists() {
420 if *filename == "pyproject.toml" {
422 if let Ok(content) = std::fs::read_to_string(&config_path) {
423 if content.contains("[tool.rumdl]") || content.contains("tool.rumdl") {
424 log::debug!("[rumdl-config] Found user configuration at: {}", config_path.display());
425 return Some(config_path);
426 }
427 log::debug!("[rumdl-config] Found user pyproject.toml but no [tool.rumdl] section");
428 continue;
429 }
430 } else {
431 log::debug!("[rumdl-config] Found user configuration at: {}", config_path.display());
432 return Some(config_path);
433 }
434 }
435 }
436
437 log::debug!(
438 "[rumdl-config] No user configuration found in: {}",
439 config_dir.display()
440 );
441 None
442 }
443
444 #[cfg(feature = "native")]
447 fn user_configuration_path() -> Option<std::path::PathBuf> {
448 use etcetera::{BaseStrategy, choose_base_strategy};
449
450 match choose_base_strategy() {
451 Ok(strategy) => {
452 let config_dir = strategy.config_dir();
453 Self::user_configuration_path_impl(&config_dir)
454 }
455 Err(e) => {
456 log::debug!("[rumdl-config] Failed to determine user config directory: {e}");
457 None
458 }
459 }
460 }
461
462 #[cfg(not(feature = "native"))]
464 fn user_configuration_path() -> Option<std::path::PathBuf> {
465 None
466 }
467
468 fn load_explicit_config(sourced_config: &mut Self, path: &str) -> Result<(), ConfigError> {
470 let path_obj = Path::new(path);
471 let filename = path_obj.file_name().and_then(|name| name.to_str()).unwrap_or("");
472 let path_str = path.to_string();
473
474 log::debug!("[rumdl-config] Loading explicit config file: {filename}");
475
476 if let Some(config_parent) = path_obj.parent() {
478 let project_root = Self::find_project_root_from(config_parent);
479 log::debug!(
480 "[rumdl-config] Project root (from explicit config): {}",
481 project_root.display()
482 );
483 sourced_config.project_root = Some(project_root);
484 }
485
486 const MARKDOWNLINT_FILENAMES: &[&str] = &[".markdownlint.json", ".markdownlint.yaml", ".markdownlint.yml"];
488
489 if filename == "pyproject.toml" || filename == ".rumdl.toml" || filename == "rumdl.toml" {
490 let content = std::fs::read_to_string(path).map_err(|e| ConfigError::IoError {
491 source: e,
492 path: path_str.clone(),
493 })?;
494 if filename == "pyproject.toml" {
495 if let Some(fragment) = parsers::parse_pyproject_toml(&content, &path_str)? {
496 sourced_config.merge(fragment);
497 sourced_config.loaded_files.push(path_str);
498 }
499 } else {
500 let fragment = parsers::parse_rumdl_toml(&content, &path_str, ConfigSource::ProjectConfig)?;
501 sourced_config.merge(fragment);
502 sourced_config.loaded_files.push(path_str);
503 }
504 } else if MARKDOWNLINT_FILENAMES.contains(&filename)
505 || path_str.ends_with(".json")
506 || path_str.ends_with(".jsonc")
507 || path_str.ends_with(".yaml")
508 || path_str.ends_with(".yml")
509 {
510 let fragment = parsers::load_from_markdownlint(&path_str)?;
512 sourced_config.merge(fragment);
513 sourced_config.loaded_files.push(path_str);
514 } else {
515 let content = std::fs::read_to_string(path).map_err(|e| ConfigError::IoError {
517 source: e,
518 path: path_str.clone(),
519 })?;
520 let fragment = parsers::parse_rumdl_toml(&content, &path_str, ConfigSource::ProjectConfig)?;
521 sourced_config.merge(fragment);
522 sourced_config.loaded_files.push(path_str);
523 }
524
525 Ok(())
526 }
527
528 fn load_user_config_as_fallback(
530 sourced_config: &mut Self,
531 user_config_dir: Option<&Path>,
532 ) -> Result<(), ConfigError> {
533 let user_config_path = if let Some(dir) = user_config_dir {
534 Self::user_configuration_path_impl(dir)
535 } else {
536 Self::user_configuration_path()
537 };
538
539 if let Some(user_config_path) = user_config_path {
540 let path_str = user_config_path.display().to_string();
541 let filename = user_config_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
542
543 log::debug!("[rumdl-config] Loading user config as fallback: {path_str}");
544
545 if filename == "pyproject.toml" {
546 let content = std::fs::read_to_string(&user_config_path).map_err(|e| ConfigError::IoError {
547 source: e,
548 path: path_str.clone(),
549 })?;
550 if let Some(fragment) = parsers::parse_pyproject_toml(&content, &path_str)? {
551 sourced_config.merge(fragment);
552 sourced_config.loaded_files.push(path_str);
553 }
554 } else {
555 let content = std::fs::read_to_string(&user_config_path).map_err(|e| ConfigError::IoError {
556 source: e,
557 path: path_str.clone(),
558 })?;
559 let fragment = parsers::parse_rumdl_toml(&content, &path_str, ConfigSource::UserConfig)?;
560 sourced_config.merge(fragment);
561 sourced_config.loaded_files.push(path_str);
562 }
563 } else {
564 log::debug!("[rumdl-config] No user configuration file found");
565 }
566
567 Ok(())
568 }
569
570 #[doc(hidden)]
572 pub fn load_with_discovery_impl(
573 config_path: Option<&str>,
574 cli_overrides: Option<&SourcedGlobalConfig>,
575 skip_auto_discovery: bool,
576 user_config_dir: Option<&Path>,
577 ) -> Result<Self, ConfigError> {
578 use std::env;
579 log::debug!("[rumdl-config] Current working directory: {:?}", env::current_dir());
580
581 let mut sourced_config = SourcedConfig::default();
582
583 if let Some(path) = config_path {
596 log::debug!("[rumdl-config] Explicit config_path provided: {path:?}");
598 Self::load_explicit_config(&mut sourced_config, path)?;
599 } else if skip_auto_discovery {
600 log::debug!("[rumdl-config] Skipping config discovery due to --no-config/--isolated flag");
601 } else {
603 log::debug!("[rumdl-config] No explicit config_path, searching default locations");
605
606 if let Some((config_file, project_root)) = Self::discover_config_upward() {
608 let path_str = config_file.display().to_string();
610 let filename = config_file.file_name().and_then(|n| n.to_str()).unwrap_or("");
611
612 log::debug!("[rumdl-config] Found project config: {path_str}");
613 log::debug!("[rumdl-config] Project root: {}", project_root.display());
614
615 sourced_config.project_root = Some(project_root);
616
617 if filename == "pyproject.toml" {
618 let content = std::fs::read_to_string(&config_file).map_err(|e| ConfigError::IoError {
619 source: e,
620 path: path_str.clone(),
621 })?;
622 if let Some(fragment) = parsers::parse_pyproject_toml(&content, &path_str)? {
623 sourced_config.merge(fragment);
624 sourced_config.loaded_files.push(path_str);
625 }
626 } else if filename == ".rumdl.toml" || filename == "rumdl.toml" {
627 let content = std::fs::read_to_string(&config_file).map_err(|e| ConfigError::IoError {
628 source: e,
629 path: path_str.clone(),
630 })?;
631 let fragment = parsers::parse_rumdl_toml(&content, &path_str, ConfigSource::ProjectConfig)?;
632 sourced_config.merge(fragment);
633 sourced_config.loaded_files.push(path_str);
634 }
635 } else {
636 log::debug!("[rumdl-config] No rumdl config found, checking markdownlint config");
638
639 if let Some(markdownlint_path) = Self::discover_markdownlint_config_upward() {
640 let path_str = markdownlint_path.display().to_string();
641 log::debug!("[rumdl-config] Found markdownlint config: {path_str}");
642 match parsers::load_from_markdownlint(&path_str) {
643 Ok(fragment) => {
644 sourced_config.merge(fragment);
645 sourced_config.loaded_files.push(path_str);
646 }
647 Err(_e) => {
648 log::debug!("[rumdl-config] Failed to load markdownlint config, trying user config");
649 Self::load_user_config_as_fallback(&mut sourced_config, user_config_dir)?;
650 }
651 }
652 } else {
653 log::debug!("[rumdl-config] No project config found, using user config as fallback");
655 Self::load_user_config_as_fallback(&mut sourced_config, user_config_dir)?;
656 }
657 }
658 }
659
660 if let Some(cli) = cli_overrides {
662 sourced_config
663 .global
664 .enable
665 .merge_override(cli.enable.value.clone(), ConfigSource::Cli, None, None);
666 sourced_config
667 .global
668 .disable
669 .merge_override(cli.disable.value.clone(), ConfigSource::Cli, None, None);
670 sourced_config
671 .global
672 .exclude
673 .merge_override(cli.exclude.value.clone(), ConfigSource::Cli, None, None);
674 sourced_config
675 .global
676 .include
677 .merge_override(cli.include.value.clone(), ConfigSource::Cli, None, None);
678 sourced_config.global.respect_gitignore.merge_override(
679 cli.respect_gitignore.value,
680 ConfigSource::Cli,
681 None,
682 None,
683 );
684 sourced_config
685 .global
686 .fixable
687 .merge_override(cli.fixable.value.clone(), ConfigSource::Cli, None, None);
688 sourced_config
689 .global
690 .unfixable
691 .merge_override(cli.unfixable.value.clone(), ConfigSource::Cli, None, None);
692 }
694
695 Ok(sourced_config)
698 }
699
700 pub fn load_with_discovery(
703 config_path: Option<&str>,
704 cli_overrides: Option<&SourcedGlobalConfig>,
705 skip_auto_discovery: bool,
706 ) -> Result<Self, ConfigError> {
707 Self::load_with_discovery_impl(config_path, cli_overrides, skip_auto_discovery, None)
708 }
709
710 pub fn validate(self, registry: &RuleRegistry) -> Result<SourcedConfig<ConfigValidated>, ConfigError> {
724 let warnings = validate_config_sourced_internal(&self, registry);
725
726 Ok(SourcedConfig {
727 global: self.global,
728 per_file_ignores: self.per_file_ignores,
729 per_file_flavor: self.per_file_flavor,
730 code_block_tools: self.code_block_tools,
731 rules: self.rules,
732 loaded_files: self.loaded_files,
733 unknown_keys: self.unknown_keys,
734 project_root: self.project_root,
735 validation_warnings: warnings,
736 _state: PhantomData,
737 })
738 }
739
740 pub fn validate_into(self, registry: &RuleRegistry) -> Result<(Config, Vec<ConfigValidationWarning>), ConfigError> {
745 let validated = self.validate(registry)?;
746 let warnings = validated.validation_warnings.clone();
747 Ok((validated.into(), warnings))
748 }
749
750 pub fn into_validated_unchecked(self) -> SourcedConfig<ConfigValidated> {
761 SourcedConfig {
762 global: self.global,
763 per_file_ignores: self.per_file_ignores,
764 per_file_flavor: self.per_file_flavor,
765 code_block_tools: self.code_block_tools,
766 rules: self.rules,
767 loaded_files: self.loaded_files,
768 unknown_keys: self.unknown_keys,
769 project_root: self.project_root,
770 validation_warnings: Vec::new(),
771 _state: PhantomData,
772 }
773 }
774}
775
776impl From<SourcedConfig<ConfigValidated>> for Config {
781 fn from(sourced: SourcedConfig<ConfigValidated>) -> Self {
782 let mut rules = BTreeMap::new();
783 for (rule_name, sourced_rule_cfg) in sourced.rules {
784 let normalized_rule_name = rule_name.to_ascii_uppercase();
786 let severity = sourced_rule_cfg.severity.map(|sv| sv.value);
787 let mut values = BTreeMap::new();
788 for (key, sourced_val) in sourced_rule_cfg.values {
789 values.insert(key, sourced_val.value);
790 }
791 rules.insert(normalized_rule_name, RuleConfig { severity, values });
792 }
793 #[allow(deprecated)]
794 let global = GlobalConfig {
795 enable: sourced.global.enable.value,
796 disable: sourced.global.disable.value,
797 exclude: sourced.global.exclude.value,
798 include: sourced.global.include.value,
799 respect_gitignore: sourced.global.respect_gitignore.value,
800 line_length: sourced.global.line_length.value,
801 output_format: sourced.global.output_format.as_ref().map(|v| v.value.clone()),
802 fixable: sourced.global.fixable.value,
803 unfixable: sourced.global.unfixable.value,
804 flavor: sourced.global.flavor.value,
805 force_exclude: sourced.global.force_exclude.value,
806 cache_dir: sourced.global.cache_dir.as_ref().map(|v| v.value.clone()),
807 cache: sourced.global.cache.value,
808 };
809 Config {
810 global,
811 per_file_ignores: sourced.per_file_ignores.value,
812 per_file_flavor: sourced.per_file_flavor.value,
813 code_block_tools: sourced.code_block_tools.value,
814 rules,
815 project_root: sourced.project_root,
816 per_file_ignores_cache: Arc::new(OnceLock::new()),
817 per_file_flavor_cache: Arc::new(OnceLock::new()),
818 }
819 }
820}