1use globset::{Glob, GlobSetBuilder};
2use serde::{Deserialize, Serialize, de::DeserializeOwned};
3use std::collections::BTreeMap;
4use std::fmt;
5use std::fs;
6use std::io;
7use std::path::{Path, PathBuf};
8
9use crate::report::Severity;
10
11pub type RuleTable = toml::Table;
12
13const CURRENT_POLICY_VERSION: u32 = 2;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
16#[serde(rename_all = "lowercase")]
17pub enum Level {
18 #[default]
19 Allow,
20 Warn,
21 Deny,
22}
23
24impl Level {
25 #[must_use]
26 pub fn enabled(self) -> bool {
27 !matches!(self, Self::Allow)
28 }
29
30 #[must_use]
31 pub fn to_severity(self) -> Severity {
32 match self {
33 Self::Allow => Severity::Info,
34 Self::Warn => Severity::Warn,
35 Self::Deny => Severity::Deny,
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
41#[serde(rename_all = "lowercase")]
42pub enum EngineMode {
43 #[default]
44 Auto,
45 Require,
46 Off,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
50#[serde(rename_all = "lowercase")]
51pub enum ToolchainMode {
52 #[default]
53 Current,
54 Auto,
55 Nightly,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
59#[serde(rename_all = "lowercase")]
60pub enum AdapterToolchainMode {
61 #[default]
62 Inherit,
63 Current,
64 Auto,
65 Nightly,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
69#[serde(rename_all = "lowercase")]
70pub enum OutputFormat {
71 #[default]
72 Text,
73 Json,
74 Sarif,
75 Html,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct EngineConfig {
80 #[serde(default)]
81 pub semantic: EngineMode,
82 #[serde(default)]
83 pub toolchain: ToolchainMode,
84 #[serde(default = "EngineConfig::default_nightly_toolchain")]
85 pub nightly_toolchain: String,
86}
87
88impl EngineConfig {
89 fn default_nightly_toolchain() -> String {
90 "nightly".to_string()
91 }
92}
93
94impl Default for EngineConfig {
95 fn default() -> Self {
96 Self {
97 semantic: EngineMode::Auto,
98 toolchain: ToolchainMode::Current,
99 nightly_toolchain: Self::default_nightly_toolchain(),
100 }
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct WorkspaceConfig {
106 #[serde(default = "WorkspaceConfig::default_include")]
107 pub include: Vec<String>,
108 #[serde(default = "WorkspaceConfig::default_exclude")]
109 pub exclude: Vec<String>,
110}
111
112impl WorkspaceConfig {
113 fn default_include() -> Vec<String> {
114 vec!["**/*.rs".to_string()]
115 }
116
117 fn default_exclude() -> Vec<String> {
118 vec!["target/**".to_string(), ".git/**".to_string()]
119 }
120}
121
122impl Default for WorkspaceConfig {
123 fn default() -> Self {
124 Self {
125 include: Self::default_include(),
126 exclude: Self::default_exclude(),
127 }
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, Default)]
132pub struct OutputConfig {
133 #[serde(default)]
134 pub format: OutputFormat,
135 #[serde(default)]
136 pub output: Option<PathBuf>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct ClippyAdapterConfig {
141 #[serde(default = "ClippyAdapterConfig::default_enabled")]
142 pub enabled: bool,
143 #[serde(default)]
144 pub args: Vec<String>,
145 #[serde(default)]
146 pub toolchain: AdapterToolchainMode,
147}
148
149impl ClippyAdapterConfig {
150 fn default_enabled() -> bool {
151 true
152 }
153}
154
155impl Default for ClippyAdapterConfig {
156 fn default() -> Self {
157 Self {
158 enabled: true,
159 args: Vec::new(),
160 toolchain: AdapterToolchainMode::Inherit,
161 }
162 }
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize, Default)]
166pub struct AdaptersConfig {
167 #[serde(default)]
168 pub clippy: ClippyAdapterConfig,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, Default)]
172pub struct RuleSettings {
173 #[serde(default)]
174 pub level: Option<Level>,
175 #[serde(flatten)]
176 pub options: RuleTable,
177}
178
179impl RuleSettings {
180 #[must_use]
181 pub fn merge(&self, override_settings: &Self) -> Self {
182 let mut options = self.options.clone();
183 merge_tables(&mut options, &override_settings.options);
184 Self {
185 level: override_settings.level.or(self.level),
186 options,
187 }
188 }
189
190 #[must_use]
191 pub fn with_default_level(mut self, default_level: Level) -> Self {
192 if self.level.is_none() {
193 self.level = Some(default_level);
194 }
195 self
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize, Default)]
200pub struct ScopeConfig {
201 #[serde(default)]
202 pub include: Vec<String>,
203 #[serde(default)]
204 pub exclude: Vec<String>,
205 #[serde(default)]
206 pub rules: BTreeMap<String, RuleSettings>,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct Policy {
211 #[serde(default = "Policy::default_version")]
212 pub version: u32,
213 #[serde(default)]
214 pub extends: Vec<PathBuf>,
215 #[serde(default)]
216 pub engine: EngineConfig,
217 #[serde(default)]
218 pub workspace: WorkspaceConfig,
219 #[serde(default)]
220 pub output: OutputConfig,
221 #[serde(default)]
222 pub adapters: AdaptersConfig,
223 #[serde(default)]
224 pub rules: BTreeMap<String, RuleSettings>,
225 #[serde(rename = "scope", default)]
226 pub scopes: Vec<ScopeConfig>,
227}
228
229impl Policy {
230 fn default_version() -> u32 {
231 CURRENT_POLICY_VERSION
232 }
233
234 #[must_use]
235 pub fn default_with_rules(
236 default_rules: impl IntoIterator<Item = (String, RuleSettings)>,
237 ) -> Self {
238 Self {
239 rules: default_rules.into_iter().collect(),
240 ..Self::default()
241 }
242 }
243
244 pub fn from_path(path: &Path) -> Result<Self, ConfigError> {
245 let table = load_policy_table(path)?;
246 validate_legacy_shape(&table, path)?;
247 let policy: Self = toml::from_str(
248 &toml::to_string(&table).map_err(ConfigError::Serialize)?,
249 )
250 .map_err(|source| ConfigError::Parse {
251 path: path.to_path_buf(),
252 source,
253 })?;
254 policy.validate(path)?;
255 Ok(policy)
256 }
257
258 pub fn validate(&self, path: &Path) -> Result<(), ConfigError> {
259 if self.version != CURRENT_POLICY_VERSION {
260 return Err(ConfigError::UnsupportedVersion {
261 path: path.to_path_buf(),
262 version: self.version,
263 });
264 }
265 Ok(())
266 }
267
268 #[must_use]
269 pub fn rule_enabled_anywhere(&self, rule_id: &str, default_level: Level) -> bool {
270 if self
271 .rule_settings(rule_id, None, default_level)
272 .level
273 .unwrap_or(default_level)
274 .enabled()
275 {
276 return true;
277 }
278 self.scopes.iter().any(|scope| {
279 scope
280 .rules
281 .get(rule_id)
282 .and_then(|settings| settings.level)
283 .unwrap_or(default_level)
284 .enabled()
285 })
286 }
287
288 #[must_use]
289 pub fn rule_settings(
290 &self,
291 rule_id: &str,
292 file_path: Option<&Path>,
293 default_level: Level,
294 ) -> RuleSettings {
295 let mut resolved = self.rules.get(rule_id).cloned().unwrap_or_default();
296 for scope in &self.scopes {
297 if !scope_matches(scope, file_path) {
298 continue;
299 }
300 if let Some(scope_settings) = scope.rules.get(rule_id) {
301 resolved = resolved.merge(scope_settings);
302 }
303 }
304 resolved.with_default_level(default_level)
305 }
306
307 pub fn decode_rule<T>(&self, rule_id: &str, file_path: Option<&Path>) -> Result<T, ConfigError>
308 where
309 T: RuleOptions,
310 {
311 let resolved = self.rule_settings(rule_id, file_path, T::default_level());
312 decode_rule_settings::<T>(&resolved).map_err(|message| ConfigError::RuleDecode {
313 rule_id: rule_id.to_string(),
314 message,
315 })
316 }
317}
318
319impl Default for Policy {
320 fn default() -> Self {
321 Self {
322 version: CURRENT_POLICY_VERSION,
323 extends: Vec::new(),
324 engine: EngineConfig::default(),
325 workspace: WorkspaceConfig::default(),
326 output: OutputConfig::default(),
327 adapters: AdaptersConfig::default(),
328 rules: BTreeMap::new(),
329 scopes: Vec::new(),
330 }
331 }
332}
333
334pub trait RuleOptions: DeserializeOwned {
335 fn default_level() -> Level;
336}
337
338pub type Config = Policy;
339
340#[derive(Debug, thiserror::Error)]
341pub enum ConfigError {
342 #[error("failed to read config file: {path}")]
343 Read { path: PathBuf, source: io::Error },
344 #[error("failed to parse config file: {path}")]
345 Parse {
346 path: PathBuf,
347 source: toml::de::Error,
348 },
349 #[error("failed to serialize policy")]
350 Serialize(#[source] toml::ser::Error),
351 #[error("failed to write config file: {path}")]
352 Write { path: PathBuf, source: io::Error },
353 #[error("policy version {version} is not supported: {path}")]
354 UnsupportedVersion { path: PathBuf, version: u32 },
355 #[error("legacy config key `{key}` is not supported in v2: {path}. {message}")]
356 LegacyKey {
357 path: PathBuf,
358 key: String,
359 message: String,
360 },
361 #[error("failed to decode rule `{rule_id}`: {message}")]
362 RuleDecode { rule_id: String, message: String },
363 #[error("failed to build glob matcher: {pattern}")]
364 Glob {
365 pattern: String,
366 #[source]
367 source: globset::Error,
368 },
369}
370
371fn decode_rule_settings<T>(settings: &RuleSettings) -> Result<T, String>
372where
373 T: RuleOptions,
374{
375 let mut table = settings.options.clone();
376 if let Some(level) = settings.level {
377 table.insert(
378 "level".to_string(),
379 toml::Value::String(level_string(level).to_string()),
380 );
381 }
382 let text = toml::to_string(&table).map_err(|err| err.to_string())?;
383 toml::from_str(&text).map_err(|err| err.to_string())
384}
385
386fn level_string(level: Level) -> &'static str {
387 match level {
388 Level::Allow => "allow",
389 Level::Warn => "warn",
390 Level::Deny => "deny",
391 }
392}
393
394fn load_policy_table(path: &Path) -> Result<RuleTable, ConfigError> {
395 let mut visiting = Vec::new();
396 load_policy_table_inner(path, &mut visiting)
397}
398
399fn load_policy_table_inner(
400 path: &Path,
401 visiting: &mut Vec<PathBuf>,
402) -> Result<RuleTable, ConfigError> {
403 let canonical = path.to_path_buf();
404 if visiting.contains(&canonical) {
405 return Ok(RuleTable::new());
406 }
407 visiting.push(canonical);
408
409 let text = fs::read_to_string(path).map_err(|source| ConfigError::Read {
410 path: path.to_path_buf(),
411 source,
412 })?;
413 let table: RuleTable = toml::from_str(&text).map_err(|source| ConfigError::Parse {
414 path: path.to_path_buf(),
415 source,
416 })?;
417
418 let mut merged = RuleTable::new();
419 if let Some(extends) = table.get("extends").and_then(toml::Value::as_array) {
420 for entry in extends {
421 let Some(relative) = entry.as_str() else {
422 continue;
423 };
424 let parent = path.parent().unwrap_or_else(|| Path::new("."));
425 let nested = load_policy_table_inner(&parent.join(relative), visiting)?;
426 merge_tables(&mut merged, &nested);
427 }
428 }
429 merge_tables(&mut merged, &table);
430 let _ = visiting.pop();
431 Ok(merged)
432}
433
434fn validate_legacy_shape(table: &RuleTable, path: &Path) -> Result<(), ConfigError> {
435 if let Some(output) = table.get("output").and_then(toml::Value::as_table) {
436 if output.get("with_clippy").is_some() {
437 return Err(ConfigError::LegacyKey {
438 path: path.to_path_buf(),
439 key: "output.with_clippy".to_string(),
440 message: "Use `[adapters.clippy].enabled`.".to_string(),
441 });
442 }
443 if output.get("format").and_then(toml::Value::as_str) == Some("human") {
444 return Err(ConfigError::LegacyKey {
445 path: path.to_path_buf(),
446 key: "output.format".to_string(),
447 message: "Use `text` instead of `human`.".to_string(),
448 });
449 }
450 }
451
452 if let Some(rules) = table.get("rules").and_then(toml::Value::as_table) {
453 for key in rules.keys() {
454 if !key.contains('.') {
455 return Err(ConfigError::LegacyKey {
456 path: path.to_path_buf(),
457 key: format!("rules.{key}"),
458 message: "Use dot rule IDs such as `shape.file_complexity`.".to_string(),
459 });
460 }
461 }
462 }
463
464 if table.get("include").is_some() || table.get("exclude").is_some() {
465 return Err(ConfigError::LegacyKey {
466 path: path.to_path_buf(),
467 key: "include/exclude".to_string(),
468 message: "Move these under `[workspace]`.".to_string(),
469 });
470 }
471 Ok(())
472}
473
474fn merge_tables(into: &mut RuleTable, overlay: &RuleTable) {
475 for (key, value) in overlay {
476 match (into.get_mut(key), value) {
477 (Some(toml::Value::Table(dst)), toml::Value::Table(src)) => merge_tables(dst, src),
478 _ => {
479 into.insert(key.clone(), value.clone());
480 }
481 }
482 }
483}
484
485fn scope_matches(scope: &ScopeConfig, file_path: Option<&Path>) -> bool {
486 let Some(file_path) = file_path else {
487 return false;
488 };
489 let display = file_path.to_string_lossy();
490
491 if !scope.include.is_empty() && !globset_matches(&scope.include, display.as_ref()) {
492 return false;
493 }
494 if !scope.exclude.is_empty() && globset_matches(&scope.exclude, display.as_ref()) {
495 return false;
496 }
497 true
498}
499
500fn globset_matches(patterns: &[String], candidate: &str) -> bool {
501 let mut builder = GlobSetBuilder::new();
502 for pattern in patterns {
503 let Ok(glob) = Glob::new(pattern) else {
504 continue;
505 };
506 builder.add(glob);
507 }
508 let Ok(set) = builder.build() else {
509 return false;
510 };
511 set.is_match(candidate)
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct AbsoluteModulePathsConfig {
516 #[serde(default = "AbsoluteModulePathsConfig::default_level")]
517 pub level: Level,
518 #[serde(default)]
519 pub allow_prefixes: Vec<String>,
520 #[serde(default = "AbsoluteModulePathsConfig::default_roots")]
521 pub roots: Vec<String>,
522 #[serde(default = "AbsoluteModulePathsConfig::default_allow_crate_root_macros")]
523 pub allow_crate_root_macros: bool,
524 #[serde(default = "AbsoluteModulePathsConfig::default_allow_crate_root_consts")]
525 pub allow_crate_root_consts: bool,
526 #[serde(default = "AbsoluteModulePathsConfig::default_allow_crate_root_fn_calls")]
527 pub allow_crate_root_fn_calls: bool,
528}
529
530impl AbsoluteModulePathsConfig {
531 fn default_level() -> Level {
532 Level::Deny
533 }
534
535 fn default_roots() -> Vec<String> {
536 vec![
537 "std".to_string(),
538 "core".to_string(),
539 "alloc".to_string(),
540 "crate".to_string(),
541 ]
542 }
543
544 fn default_allow_crate_root_macros() -> bool {
545 true
546 }
547
548 fn default_allow_crate_root_consts() -> bool {
549 true
550 }
551
552 fn default_allow_crate_root_fn_calls() -> bool {
553 true
554 }
555}
556
557impl Default for AbsoluteModulePathsConfig {
558 fn default() -> Self {
559 Self {
560 level: Self::default_level(),
561 allow_prefixes: Vec::new(),
562 roots: Self::default_roots(),
563 allow_crate_root_macros: true,
564 allow_crate_root_consts: true,
565 allow_crate_root_fn_calls: true,
566 }
567 }
568}
569
570impl RuleOptions for AbsoluteModulePathsConfig {
571 fn default_level() -> Level {
572 Self::default_level()
573 }
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize)]
577pub struct AbsoluteFilesystemPathsConfig {
578 #[serde(default = "AbsoluteFilesystemPathsConfig::default_level")]
579 pub level: Level,
580 #[serde(default)]
581 pub allow_globs: Vec<String>,
582 #[serde(default)]
583 pub allow_regex: Vec<String>,
584 #[serde(default)]
585 pub check_comments: bool,
586}
587
588impl AbsoluteFilesystemPathsConfig {
589 fn default_level() -> Level {
590 Level::Warn
591 }
592}
593
594impl Default for AbsoluteFilesystemPathsConfig {
595 fn default() -> Self {
596 Self {
597 level: Self::default_level(),
598 allow_globs: Vec::new(),
599 allow_regex: Vec::new(),
600 check_comments: false,
601 }
602 }
603}
604
605impl RuleOptions for AbsoluteFilesystemPathsConfig {
606 fn default_level() -> Level {
607 Self::default_level()
608 }
609}
610
611#[derive(Debug, Clone, Serialize, Deserialize)]
612#[serde(rename_all = "snake_case")]
613pub enum ComplexityMode {
614 Cyclomatic,
615 PhysicalLoc,
616 LogicalLoc,
617}
618
619#[derive(Debug, Clone, Serialize, Deserialize)]
620pub struct FileComplexityConfig {
621 #[serde(default = "FileComplexityConfig::default_level")]
622 pub level: Level,
623 #[serde(default = "FileComplexityConfig::default_mode")]
624 pub mode: ComplexityMode,
625 #[serde(default = "FileComplexityConfig::default_max_file")]
626 pub max_file: u32,
627 #[serde(default = "FileComplexityConfig::default_max_fn")]
628 pub max_fn: u32,
629 #[serde(default = "FileComplexityConfig::default_count_question")]
630 pub count_question: bool,
631 #[serde(default = "FileComplexityConfig::default_match_arms")]
632 pub match_arms: bool,
633}
634
635impl FileComplexityConfig {
636 fn default_level() -> Level {
637 Level::Warn
638 }
639 fn default_mode() -> ComplexityMode {
640 ComplexityMode::Cyclomatic
641 }
642 fn default_max_file() -> u32 {
643 200
644 }
645 fn default_max_fn() -> u32 {
646 25
647 }
648 fn default_count_question() -> bool {
649 false
650 }
651 fn default_match_arms() -> bool {
652 true
653 }
654}
655
656impl Default for FileComplexityConfig {
657 fn default() -> Self {
658 Self {
659 level: Self::default_level(),
660 mode: Self::default_mode(),
661 max_file: 200,
662 max_fn: 25,
663 count_question: false,
664 match_arms: true,
665 }
666 }
667}
668
669impl RuleOptions for FileComplexityConfig {
670 fn default_level() -> Level {
671 Self::default_level()
672 }
673}
674
675#[derive(Debug, Clone, Serialize, Deserialize)]
676pub struct DuplicateLogicConfig {
677 #[serde(default = "DuplicateLogicConfig::default_level")]
678 pub level: Level,
679 #[serde(default = "DuplicateLogicConfig::default_min_tokens")]
680 pub min_tokens: usize,
681 #[serde(default = "DuplicateLogicConfig::default_threshold")]
682 pub threshold: f32,
683 #[serde(default = "DuplicateLogicConfig::default_max_results")]
684 pub max_results: usize,
685 #[serde(default)]
686 pub exclude_globs: Vec<String>,
687 #[serde(default = "DuplicateLogicConfig::default_kgram")]
688 pub kgram: usize,
689}
690
691impl DuplicateLogicConfig {
692 fn default_level() -> Level {
693 Level::Warn
694 }
695 fn default_min_tokens() -> usize {
696 80
697 }
698 fn default_threshold() -> f32 {
699 0.80
700 }
701 fn default_max_results() -> usize {
702 200
703 }
704 fn default_kgram() -> usize {
705 25
706 }
707}
708
709impl Default for DuplicateLogicConfig {
710 fn default() -> Self {
711 Self {
712 level: Self::default_level(),
713 min_tokens: 80,
714 threshold: 0.80,
715 max_results: 200,
716 exclude_globs: Vec::new(),
717 kgram: 25,
718 }
719 }
720}
721
722impl RuleOptions for DuplicateLogicConfig {
723 fn default_level() -> Level {
724 Self::default_level()
725 }
726}
727
728#[derive(Debug, Clone, Serialize, Deserialize)]
729pub struct DuplicateTypesAliasConfig {
730 #[serde(default = "DuplicateTypesAliasConfig::default_level")]
731 pub level: Level,
732 #[serde(default = "DuplicateTypesAliasConfig::default_min_occurrences")]
733 pub min_occurrences: usize,
734 #[serde(default = "DuplicateTypesAliasConfig::default_min_len")]
735 pub min_len: usize,
736 #[serde(default = "DuplicateTypesAliasConfig::default_exclude_outer")]
737 pub exclude_outer: Vec<String>,
738}
739
740impl DuplicateTypesAliasConfig {
741 fn default_level() -> Level {
742 Level::Allow
743 }
744 fn default_min_occurrences() -> usize {
745 3
746 }
747 fn default_min_len() -> usize {
748 25
749 }
750 fn default_exclude_outer() -> Vec<String> {
751 vec!["Option".to_string()]
752 }
753}
754
755impl Default for DuplicateTypesAliasConfig {
756 fn default() -> Self {
757 Self {
758 level: Self::default_level(),
759 min_occurrences: 3,
760 min_len: 25,
761 exclude_outer: Self::default_exclude_outer(),
762 }
763 }
764}
765
766impl RuleOptions for DuplicateTypesAliasConfig {
767 fn default_level() -> Level {
768 Self::default_level()
769 }
770}
771
772#[derive(Debug, Clone, Serialize, Deserialize)]
773pub struct SrpHeuristicConfig {
774 #[serde(default)]
775 pub level: Level,
776 #[serde(default = "SrpHeuristicConfig::default_method_count")]
777 pub method_count_threshold: usize,
778}
779
780impl SrpHeuristicConfig {
781 fn default_method_count() -> usize {
782 25
783 }
784}
785
786impl Default for SrpHeuristicConfig {
787 fn default() -> Self {
788 Self {
789 level: Level::Allow,
790 method_count_threshold: 25,
791 }
792 }
793}
794
795impl RuleOptions for SrpHeuristicConfig {
796 fn default_level() -> Level {
797 Level::Allow
798 }
799}
800
801#[derive(Debug, Clone, Serialize, Deserialize)]
802pub struct BannedDependenciesConfig {
803 #[serde(default = "BannedDependenciesConfig::default_level")]
804 pub level: Level,
805 #[serde(default)]
806 pub banned_prefixes: Vec<String>,
807}
808
809impl BannedDependenciesConfig {
810 fn default_level() -> Level {
811 Level::Allow
812 }
813}
814
815impl Default for BannedDependenciesConfig {
816 fn default() -> Self {
817 Self {
818 level: Self::default_level(),
819 banned_prefixes: Vec::new(),
820 }
821 }
822}
823
824impl RuleOptions for BannedDependenciesConfig {
825 fn default_level() -> Level {
826 Self::default_level()
827 }
828}
829
830#[derive(Debug, Clone, Serialize, Deserialize)]
831pub struct PublicApiErrorsConfig {
832 #[serde(default = "PublicApiErrorsConfig::default_level")]
833 pub level: Level,
834 #[serde(default = "PublicApiErrorsConfig::default_allowed_error_types")]
835 pub allowed_error_types: Vec<String>,
836}
837
838impl PublicApiErrorsConfig {
839 fn default_level() -> Level {
840 Level::Allow
841 }
842
843 fn default_allowed_error_types() -> Vec<String> {
844 vec![
845 "crate::Error".to_string(),
846 "crate::error::Error".to_string(),
847 ]
848 }
849}
850
851impl Default for PublicApiErrorsConfig {
852 fn default() -> Self {
853 Self {
854 level: Self::default_level(),
855 allowed_error_types: Self::default_allowed_error_types(),
856 }
857 }
858}
859
860impl RuleOptions for PublicApiErrorsConfig {
861 fn default_level() -> Level {
862 Self::default_level()
863 }
864}
865
866#[derive(Debug, Clone, Serialize, Deserialize)]
867pub struct LayerRuleSet {
868 pub name: String,
869 #[serde(default)]
870 pub include: Vec<String>,
871 #[serde(default)]
872 pub may_depend_on: Vec<String>,
873}
874
875#[derive(Debug, Clone, Serialize, Deserialize)]
876pub struct LayerDirectionConfig {
877 #[serde(default = "LayerDirectionConfig::default_level")]
878 pub level: Level,
879 #[serde(default)]
880 pub layers: Vec<LayerRuleSet>,
881}
882
883impl LayerDirectionConfig {
884 fn default_level() -> Level {
885 Level::Allow
886 }
887}
888
889impl Default for LayerDirectionConfig {
890 fn default() -> Self {
891 Self {
892 level: Self::default_level(),
893 layers: Vec::new(),
894 }
895 }
896}
897
898impl RuleOptions for LayerDirectionConfig {
899 fn default_level() -> Level {
900 Self::default_level()
901 }
902}
903
904impl fmt::Display for OutputFormat {
905 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
906 match self {
907 Self::Text => f.write_str("text"),
908 Self::Json => f.write_str("json"),
909 Self::Sarif => f.write_str("sarif"),
910 Self::Html => f.write_str("html"),
911 }
912 }
913}
914
915#[cfg(test)]
916mod tests {
917 use super::{ConfigError, Level, Policy};
918 use std::fs;
919
920 #[test]
921 fn rejects_legacy_human_format() {
922 let dir = tempfile::tempdir().unwrap();
923 let path = dir.path().join(".rscheck.toml");
924 fs::write(&path, "[output]\nformat = \"human\"\n").unwrap();
925
926 let err = Policy::from_path(&path).unwrap_err();
927 assert!(matches!(err, ConfigError::LegacyKey { .. }));
928 assert!(err.to_string().contains("text"));
929 }
930
931 #[test]
932 fn merges_extended_policy() {
933 let dir = tempfile::tempdir().unwrap();
934 let base = dir.path().join("base.toml");
935 let child = dir.path().join("child.toml");
936
937 fs::write(
938 &base,
939 "version = 2\n[rules.\"shape.file_complexity\"]\nlevel = \"warn\"\nmax_file = 10\n",
940 )
941 .unwrap();
942 fs::write(
943 &child,
944 "version = 2\nextends = [\"base.toml\"]\n[rules.\"shape.file_complexity\"]\nmax_fn = 2\n",
945 )
946 .unwrap();
947
948 let policy = Policy::from_path(&child).unwrap();
949 let settings = policy.rule_settings("shape.file_complexity", None, Level::Warn);
950 assert_eq!(settings.level, Some(Level::Warn));
951 assert_eq!(
952 settings
953 .options
954 .get("max_file")
955 .and_then(toml::Value::as_integer),
956 Some(10)
957 );
958 assert_eq!(
959 settings
960 .options
961 .get("max_fn")
962 .and_then(toml::Value::as_integer),
963 Some(2)
964 );
965 }
966}