1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9
10use ryo_suggest::SuggestStrategy;
11
12#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14#[serde(default)]
15pub struct RyoConfig {
16 pub project: ProjectConfig,
18
19 #[serde(default)]
21 pub modules: HashMap<String, ModuleConfig>,
22
23 #[serde(default)]
25 pub import: ImportConfig,
26
27 #[serde(default)]
29 pub mutations: MutationConfig,
30
31 #[serde(default)]
33 pub suggest: SuggestConfig,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(default)]
39pub struct ProjectConfig {
40 pub name: Option<String>,
42
43 pub description: Option<String>,
45
46 #[serde(default = "default_edition")]
48 pub edition: String,
49
50 pub workspace_root: Option<PathBuf>,
55
56 pub manifest_path: Option<PathBuf>,
60}
61
62impl Default for ProjectConfig {
63 fn default() -> Self {
64 Self {
65 name: None,
66 description: None,
67 edition: default_edition(),
68 workspace_root: None,
69 manifest_path: None,
70 }
71 }
72}
73
74fn default_edition() -> String {
75 "2021".to_string()
76}
77
78#[derive(Debug, Clone, Default, Serialize, Deserialize)]
89#[serde(default)]
90pub struct ModuleConfig {
91 pub skip_lint: bool,
93
94 pub skip_refactor: bool,
96
97 pub allow_unsafe: bool,
99
100 pub skip_format_check: bool,
102
103 #[serde(default)]
105 pub tags: Vec<String>,
106
107 #[serde(default)]
109 pub disabled_rules: Vec<String>,
110
111 #[serde(default)]
113 pub enabled_rules: Vec<String>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118#[serde(default)]
119pub struct ImportConfig {
120 pub preserve_comments: bool,
122
123 pub auto_format: bool,
125
126 pub validate_syntax: bool,
128}
129
130impl Default for ImportConfig {
131 fn default() -> Self {
132 Self {
133 preserve_comments: true,
134 auto_format: false,
135 validate_syntax: true,
136 }
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142#[serde(default)]
143pub struct MutationConfig {
144 pub auto_organize_imports: bool,
146
147 pub check_compile: bool,
149
150 pub parallel: bool,
152}
153
154impl Default for MutationConfig {
155 fn default() -> Self {
156 Self {
157 auto_organize_imports: false,
158 check_compile: false,
159 parallel: true,
160 }
161 }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166#[serde(default)]
167pub struct SuggestConfig {
168 pub strategy: String,
170
171 pub auto_detect_after_run: bool,
173
174 pub auto_apply: bool,
176
177 #[serde(default)]
179 pub enabled_patterns: Vec<String>,
180
181 #[serde(default)]
183 pub disabled_patterns: Vec<String>,
184
185 #[serde(default)]
188 pub disabled_rules: Vec<String>,
189
190 #[serde(default)]
193 pub enabled_rules: Vec<String>,
194
195 #[serde(default)]
198 pub severity_overrides: std::collections::HashMap<String, String>,
199}
200
201impl Default for SuggestConfig {
202 fn default() -> Self {
203 Self {
204 strategy: "interactive".to_string(),
205 auto_detect_after_run: true,
206 auto_apply: false,
207 enabled_patterns: vec![],
208 disabled_patterns: vec![],
209 disabled_rules: vec![],
210 enabled_rules: vec![],
211 severity_overrides: std::collections::HashMap::new(),
212 }
213 }
214}
215
216impl SuggestConfig {
217 pub fn to_strategy(&self) -> SuggestStrategy {
219 match self.strategy.as_str() {
220 "high_perf" => SuggestStrategy::high_perf(),
221 "batch" | "manual" => SuggestStrategy::batch(),
222 _ => SuggestStrategy::interactive(),
223 }
224 }
225
226 pub fn is_pattern_enabled(&self, name: &str) -> bool {
228 if self.disabled_patterns.iter().any(|p| p == name) {
230 return false;
231 }
232
233 if self.enabled_patterns.is_empty() {
235 return true;
236 }
237
238 self.enabled_patterns.iter().any(|p| p == name)
240 }
241
242 pub fn is_rule_enabled(&self, rule_id: &str) -> bool {
244 if self
246 .disabled_rules
247 .iter()
248 .any(|p| Self::matches_pattern(p, rule_id))
249 {
250 return false;
251 }
252
253 if self.enabled_rules.is_empty() {
255 return true;
256 }
257
258 self.enabled_rules
260 .iter()
261 .any(|p| Self::matches_pattern(p, rule_id))
262 }
263
264 pub fn get_severity_override(&self, rule_id: &str) -> Option<&str> {
266 self.severity_overrides.get(rule_id).map(|s| s.as_str())
267 }
268
269 fn matches_pattern(pattern: &str, value: &str) -> bool {
271 if pattern == "*" {
272 return true;
273 }
274 if let Some(prefix) = pattern.strip_suffix('*') {
275 return value.starts_with(prefix);
276 }
277 if let Some(suffix) = pattern.strip_prefix('*') {
278 return value.ends_with(suffix);
279 }
280 pattern == value
281 }
282}
283
284#[derive(Debug, thiserror::Error)]
286pub enum ConfigError {
287 #[error("IO error: {0}")]
288 Io(#[from] std::io::Error),
289
290 #[error("TOML parse error: {0}")]
291 Toml(#[from] toml::de::Error),
292
293 #[error("Config not found: {0}")]
294 NotFound(PathBuf),
295}
296
297impl RyoConfig {
298 pub const FILE_NAME: &'static str = "ryo.toml";
300
301 pub fn load(dir: impl AsRef<Path>) -> Result<Self, ConfigError> {
303 let path = dir.as_ref().join(Self::FILE_NAME);
304 Self::load_from_path(&path)
305 }
306
307 pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
309 let path = path.as_ref();
310 if !path.exists() {
311 return Err(ConfigError::NotFound(path.to_path_buf()));
312 }
313
314 let content = std::fs::read_to_string(path)?;
315 let config: RyoConfig = toml::from_str(&content)?;
316 Ok(config)
317 }
318
319 pub fn load_or_default(dir: impl AsRef<Path>) -> Self {
321 Self::load(dir).unwrap_or_default()
322 }
323
324 pub fn save(&self, dir: impl AsRef<Path>) -> Result<(), ConfigError> {
326 let path = dir.as_ref().join(Self::FILE_NAME);
327 self.save_to_path(&path)
328 }
329
330 pub fn save_to_path(&self, path: impl AsRef<Path>) -> Result<(), ConfigError> {
332 let content = toml::to_string_pretty(self).map_err(std::io::Error::other)?;
333 std::fs::write(path, content)?;
334 Ok(())
335 }
336
337 pub fn get_module_config(&self, path: &str) -> Option<&ModuleConfig> {
342 if let Some(config) = self.modules.get(path) {
344 if !path.contains("::") {
345 return Some(config);
346 }
347 }
348
349 for (key, config) in &self.modules {
352 if key.contains("::") {
353 continue; }
355 if path.starts_with(key) {
356 return Some(config);
357 }
358 }
359
360 None
361 }
362
363 pub fn get_module_config_for_symbol(&self, symbol_path: &str) -> Option<&ModuleConfig> {
373 if let Some(config) = self.modules.get(symbol_path) {
375 return Some(config);
376 }
377
378 for (pattern, config) in &self.modules {
380 if !pattern.contains("::") {
381 continue; }
383 if Self::matches_symbol_path_pattern(pattern, symbol_path) {
384 return Some(config);
385 }
386 }
387
388 None
389 }
390
391 fn matches_symbol_path_pattern(pattern: &str, symbol_path: &str) -> bool {
396 let pattern_parts: Vec<&str> = pattern.split("::").collect();
397 let path_parts: Vec<&str> = symbol_path.split("::").collect();
398
399 let ends_with_wildcard = pattern_parts.last() == Some(&"*");
401 let pattern_parts = if ends_with_wildcard {
402 &pattern_parts[..pattern_parts.len() - 1]
403 } else {
404 &pattern_parts[..]
405 };
406
407 if !ends_with_wildcard && pattern_parts.len() != path_parts.len() {
409 return false;
410 }
411 if pattern_parts.len() > path_parts.len() {
412 return false;
413 }
414
415 for (i, pattern_part) in pattern_parts.iter().enumerate() {
417 if *pattern_part == "*" {
418 continue; }
420 if path_parts.get(i) != Some(pattern_part) {
421 return false;
422 }
423 }
424
425 true
426 }
427
428 pub fn should_skip_lint(&self, path: &str) -> bool {
430 self.get_module_config(path)
431 .map(|c| c.skip_lint)
432 .unwrap_or(false)
433 }
434
435 pub fn should_skip_refactor(&self, path: &str) -> bool {
437 self.get_module_config(path)
438 .map(|c| c.skip_refactor)
439 .unwrap_or(false)
440 }
441
442 pub fn is_rule_enabled_for_symbol(&self, symbol_path: &str, rule_id: &str) -> bool {
446 if let Some(module_config) = self.get_module_config_for_symbol(symbol_path) {
448 if module_config
450 .disabled_rules
451 .iter()
452 .any(|p| SuggestConfig::matches_pattern(p, rule_id))
453 {
454 return false;
455 }
456 if !module_config.enabled_rules.is_empty()
458 && !module_config
459 .enabled_rules
460 .iter()
461 .any(|p| SuggestConfig::matches_pattern(p, rule_id))
462 {
463 return false;
464 }
465 }
466
467 self.suggest.is_rule_enabled(rule_id)
469 }
470
471 pub fn is_rule_enabled_for_file(&self, file_path: &str, rule_id: &str) -> bool {
476 if let Some(module_config) = self.get_module_config(file_path) {
478 if module_config
480 .disabled_rules
481 .iter()
482 .any(|p| SuggestConfig::matches_pattern(p, rule_id))
483 {
484 return false;
485 }
486 if !module_config.enabled_rules.is_empty()
488 && !module_config
489 .enabled_rules
490 .iter()
491 .any(|p| SuggestConfig::matches_pattern(p, rule_id))
492 {
493 return false;
494 }
495 }
496
497 self.suggest.is_rule_enabled(rule_id)
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505
506 #[test]
507 fn test_parse_minimal_config() {
508 let toml = r#"
509[project]
510name = "my-app"
511"#;
512 let config: RyoConfig = toml::from_str(toml).unwrap();
513 assert_eq!(config.project.name, Some("my-app".to_string()));
514 }
515
516 #[test]
517 fn test_parse_full_config() {
518 let toml = r#"
519[project]
520name = "my-app"
521description = "A sample project"
522edition = "2021"
523
524[modules."src/generated"]
525skip_lint = true
526allow_unsafe = true
527
528[modules."src/legacy"]
529skip_refactor = true
530tags = ["deprecated"]
531
532[import]
533preserve_comments = true
534auto_format = false
535
536[mutations]
537auto_organize_imports = true
538check_compile = true
539parallel = true
540"#;
541 let config: RyoConfig = toml::from_str(toml).unwrap();
542
543 assert_eq!(config.project.name, Some("my-app".to_string()));
544
545 let gen_config = config.modules.get("src/generated").unwrap();
546 assert!(gen_config.skip_lint);
547 assert!(gen_config.allow_unsafe);
548
549 let legacy_config = config.modules.get("src/legacy").unwrap();
550 assert!(legacy_config.skip_refactor);
551 assert_eq!(legacy_config.tags, vec!["deprecated"]);
552
553 assert!(config.import.preserve_comments);
554 assert!(config.mutations.auto_organize_imports);
555 }
556
557 #[test]
558 fn test_default_config() {
559 let config = RyoConfig::default();
560 assert_eq!(config.project.edition, "2021");
561 assert!(config.import.preserve_comments);
562 assert!(config.mutations.parallel);
563 }
564
565 #[test]
566 fn test_module_config_prefix_match() {
567 let toml = r#"
568[modules."src/generated"]
569skip_lint = true
570"#;
571 let config: RyoConfig = toml::from_str(toml).unwrap();
572
573 assert!(config.should_skip_lint("src/generated"));
575
576 assert!(config.should_skip_lint("src/generated/foo.rs"));
578 assert!(config.should_skip_lint("src/generated/bar/baz.rs"));
579
580 assert!(!config.should_skip_lint("src/main.rs"));
582 }
583
584 #[test]
585 fn test_suggest_config_default() {
586 let config = SuggestConfig::default();
587 assert_eq!(config.strategy, "interactive");
588 assert!(config.auto_detect_after_run);
589 assert!(!config.auto_apply);
590 assert!(config.enabled_patterns.is_empty());
591 assert!(config.disabled_patterns.is_empty());
592 }
593
594 #[test]
595 fn test_suggest_config_parse() {
596 let toml = r#"
597[suggest]
598strategy = "high_perf"
599auto_detect_after_run = false
600auto_apply = false
601disabled_patterns = ["Builder"]
602"#;
603 let config: RyoConfig = toml::from_str(toml).unwrap();
604 assert_eq!(config.suggest.strategy, "high_perf");
605 assert!(!config.suggest.auto_detect_after_run);
606 assert_eq!(config.suggest.disabled_patterns, vec!["Builder"]);
607 }
608
609 #[test]
610 fn test_suggest_config_to_strategy() {
611 let config = SuggestConfig {
612 strategy: "interactive".to_string(),
613 ..Default::default()
614 };
615 let _ = config.to_strategy(); let config = SuggestConfig {
618 strategy: "high_perf".to_string(),
619 ..Default::default()
620 };
621 let _ = config.to_strategy();
622
623 let config = SuggestConfig {
624 strategy: "batch".to_string(),
625 ..Default::default()
626 };
627 let _ = config.to_strategy();
628 }
629
630 #[test]
631 fn test_suggest_config_pattern_enabled() {
632 let config = SuggestConfig::default();
634 assert!(config.is_pattern_enabled("Default"));
635 assert!(config.is_pattern_enabled("Builder"));
636
637 let config = SuggestConfig {
639 enabled_patterns: vec!["Default".to_string()],
640 ..Default::default()
641 };
642 assert!(config.is_pattern_enabled("Default"));
643 assert!(!config.is_pattern_enabled("Builder"));
644
645 let config = SuggestConfig {
647 disabled_patterns: vec!["Builder".to_string()],
648 ..Default::default()
649 };
650 assert!(config.is_pattern_enabled("Default"));
651 assert!(!config.is_pattern_enabled("Builder"));
652
653 let config = SuggestConfig {
655 enabled_patterns: vec!["Default".to_string(), "Builder".to_string()],
656 disabled_patterns: vec!["Builder".to_string()],
657 ..Default::default()
658 };
659 assert!(config.is_pattern_enabled("Default"));
660 assert!(!config.is_pattern_enabled("Builder"));
661 }
662
663 #[test]
664 fn test_suggest_config_rule_enabled() {
665 let config = SuggestConfig::default();
667 assert!(config.is_rule_enabled("RL001"));
668 assert!(config.is_rule_enabled("RL090"));
669
670 let config = SuggestConfig {
672 disabled_rules: vec!["RL001".to_string()],
673 ..Default::default()
674 };
675 assert!(!config.is_rule_enabled("RL001"));
676 assert!(config.is_rule_enabled("RL002"));
677
678 let config = SuggestConfig {
680 disabled_rules: vec!["RL09*".to_string()],
681 ..Default::default()
682 };
683 assert!(config.is_rule_enabled("RL001"));
684 assert!(!config.is_rule_enabled("RL090"));
685 assert!(!config.is_rule_enabled("RL091"));
686
687 let config = SuggestConfig {
689 enabled_rules: vec!["RL00*".to_string()],
690 ..Default::default()
691 };
692 assert!(config.is_rule_enabled("RL001"));
693 assert!(config.is_rule_enabled("RL002"));
694 assert!(!config.is_rule_enabled("RL010"));
695 assert!(!config.is_rule_enabled("RL090"));
696
697 let config = SuggestConfig {
699 enabled_rules: vec!["RL00*".to_string()],
700 disabled_rules: vec!["RL002".to_string()],
701 ..Default::default()
702 };
703 assert!(config.is_rule_enabled("RL001"));
704 assert!(!config.is_rule_enabled("RL002"));
705 assert!(!config.is_rule_enabled("RL010"));
706 }
707
708 #[test]
709 fn test_suggest_config_matches_pattern() {
710 assert!(SuggestConfig::matches_pattern("*", "anything"));
711 assert!(SuggestConfig::matches_pattern("RL*", "RL001"));
712 assert!(SuggestConfig::matches_pattern("RL*", "RL999"));
713 assert!(!SuggestConfig::matches_pattern("RL*", "XX001"));
714 assert!(SuggestConfig::matches_pattern("*001", "RL001"));
715 assert!(!SuggestConfig::matches_pattern("*001", "RL002"));
716 assert!(SuggestConfig::matches_pattern("RL001", "RL001"));
717 assert!(!SuggestConfig::matches_pattern("RL001", "RL002"));
718 }
719
720 #[test]
721 fn test_suggest_config_severity_overrides() {
722 let mut overrides = std::collections::HashMap::new();
723 overrides.insert("RL001".to_string(), "Error".to_string());
724 overrides.insert("RL090".to_string(), "Warning".to_string());
725
726 let config = SuggestConfig {
727 severity_overrides: overrides,
728 ..Default::default()
729 };
730
731 assert_eq!(config.get_severity_override("RL001"), Some("Error"));
733 assert_eq!(config.get_severity_override("RL090"), Some("Warning"));
734
735 assert_eq!(config.get_severity_override("RL002"), None);
737 }
738
739 #[test]
740 fn test_suggest_config_severity_parse() {
741 let toml = r#"
742[suggest]
743severity_overrides = { "RL001" = "Error", "RL021" = "Info" }
744"#;
745 let config: crate::config::RyoConfig = toml::from_str(toml).unwrap();
746 assert_eq!(config.suggest.get_severity_override("RL001"), Some("Error"));
747 assert_eq!(config.suggest.get_severity_override("RL021"), Some("Info"));
748 assert_eq!(config.suggest.get_severity_override("RL999"), None);
749 }
750
751 #[test]
754 fn test_matches_symbol_path_pattern_exact() {
755 assert!(RyoConfig::matches_symbol_path_pattern(
757 "my_crate::module::Symbol",
758 "my_crate::module::Symbol"
759 ));
760 assert!(!RyoConfig::matches_symbol_path_pattern(
762 "my_crate::module::Symbol",
763 "my_crate::module::Other"
764 ));
765 assert!(!RyoConfig::matches_symbol_path_pattern(
767 "my_crate::module",
768 "my_crate::module::Symbol"
769 ));
770 }
771
772 #[test]
773 fn test_matches_symbol_path_pattern_trailing_wildcard() {
774 assert!(RyoConfig::matches_symbol_path_pattern(
776 "my_crate::generated::*",
777 "my_crate::generated::Foo"
778 ));
779 assert!(RyoConfig::matches_symbol_path_pattern(
780 "my_crate::generated::*",
781 "my_crate::generated::sub::Bar"
782 ));
783 assert!(!RyoConfig::matches_symbol_path_pattern(
785 "my_crate::generated::*",
786 "my_crate::other::Foo"
787 ));
788 assert!(RyoConfig::matches_symbol_path_pattern(
790 "my_crate::*",
791 "my_crate::module::sub::Symbol"
792 ));
793 }
794
795 #[test]
796 fn test_matches_symbol_path_pattern_segment_wildcard() {
797 assert!(RyoConfig::matches_symbol_path_pattern(
799 "*::tests::*",
800 "my_crate::tests::test_foo"
801 ));
802 assert!(RyoConfig::matches_symbol_path_pattern(
803 "*::tests::*",
804 "other_crate::tests::test_bar"
805 ));
806 assert!(!RyoConfig::matches_symbol_path_pattern(
808 "*::tests::*",
809 "my_crate::module::Symbol"
810 ));
811 assert!(RyoConfig::matches_symbol_path_pattern(
813 "*::generated::Foo",
814 "any_crate::generated::Foo"
815 ));
816 }
817
818 #[test]
819 fn test_get_module_config_for_symbol_exact() {
820 let toml = r#"
821[modules."my_crate::generated"]
822skip_lint = true
823disabled_rules = ["RL001"]
824"#;
825 let config: RyoConfig = toml::from_str(toml).unwrap();
826
827 let module_config = config.get_module_config_for_symbol("my_crate::generated");
829 assert!(module_config.is_some());
830 assert!(module_config.unwrap().skip_lint);
831
832 let no_match = config.get_module_config_for_symbol("my_crate::generated::Foo");
834 assert!(no_match.is_none());
835 }
836
837 #[test]
838 fn test_get_module_config_for_symbol_with_wildcard() {
839 let toml = r#"
840[modules."my_crate::generated::*"]
841skip_lint = true
842disabled_rules = ["RL090", "RL091"]
843"#;
844 let config: RyoConfig = toml::from_str(toml).unwrap();
845
846 let module_config = config.get_module_config_for_symbol("my_crate::generated::Foo");
848 assert!(module_config.is_some());
849 assert!(module_config.unwrap().skip_lint);
850
851 let deep = config.get_module_config_for_symbol("my_crate::generated::sub::Bar");
853 assert!(deep.is_some());
854
855 let no_match = config.get_module_config_for_symbol("my_crate::other::Foo");
857 assert!(no_match.is_none());
858 }
859
860 #[test]
861 fn test_get_module_config_file_path_vs_symbol_path() {
862 let toml = r#"
863[modules."src/generated"]
864skip_lint = true
865
866[modules."my_crate::generated::*"]
867skip_refactor = true
868"#;
869 let config: RyoConfig = toml::from_str(toml).unwrap();
870
871 let file_config = config.get_module_config("src/generated/foo.rs");
873 assert!(file_config.is_some());
874 assert!(file_config.unwrap().skip_lint);
875 assert!(!file_config.unwrap().skip_refactor);
876
877 let symbol_config = config.get_module_config_for_symbol("my_crate::generated::Foo");
879 assert!(symbol_config.is_some());
880 assert!(symbol_config.unwrap().skip_refactor);
881 assert!(!symbol_config.unwrap().skip_lint);
882
883 let no_file_match = config.get_module_config("my_crate::generated::Foo");
885 assert!(no_file_match.is_none());
886
887 let no_symbol_match = config.get_module_config_for_symbol("src/generated/foo.rs");
889 assert!(no_symbol_match.is_none());
890 }
891
892 #[test]
893 fn test_is_rule_enabled_for_symbol_module_override() {
894 let toml = r#"
895[suggest]
896disabled_rules = ["RL001"]
897
898[modules."my_crate::generated::*"]
899disabled_rules = ["RL090", "RL091"]
900"#;
901 let config: RyoConfig = toml::from_str(toml).unwrap();
902
903 assert!(!config.is_rule_enabled_for_symbol("any::path", "RL001"));
905
906 assert!(!config.is_rule_enabled_for_symbol("my_crate::generated::Foo", "RL090"));
908 assert!(!config.is_rule_enabled_for_symbol("my_crate::generated::Foo", "RL091"));
909
910 assert!(config.is_rule_enabled_for_symbol("my_crate::generated::Foo", "RL002"));
912
913 assert!(config.is_rule_enabled_for_symbol("my_crate::other::Bar", "RL090"));
915 }
916
917 #[test]
918 fn test_is_rule_enabled_for_symbol_with_enabled_rules() {
919 let toml = r#"
920[modules."my_crate::special::*"]
921enabled_rules = ["RL001", "RL002"]
922"#;
923 let config: RyoConfig = toml::from_str(toml).unwrap();
924
925 assert!(config.is_rule_enabled_for_symbol("my_crate::special::Foo", "RL001"));
927 assert!(config.is_rule_enabled_for_symbol("my_crate::special::Foo", "RL002"));
928 assert!(!config.is_rule_enabled_for_symbol("my_crate::special::Foo", "RL003"));
929
930 assert!(config.is_rule_enabled_for_symbol("my_crate::other::Bar", "RL003"));
932 }
933
934 #[test]
935 fn test_module_config_disabled_rules_with_wildcard() {
936 let toml = r#"
937[modules."*::tests::*"]
938disabled_rules = ["RL*"]
939"#;
940 let config: RyoConfig = toml::from_str(toml).unwrap();
941
942 assert!(!config.is_rule_enabled_for_symbol("my_crate::tests::test_foo", "RL001"));
944 assert!(!config.is_rule_enabled_for_symbol("other::tests::test_bar", "RL999"));
945
946 assert!(config.is_rule_enabled_for_symbol("my_crate::lib::Foo", "RL001"));
948 }
949}