Skip to main content

depguard_check_catalog/
lib.rs

1//! Check catalog metadata and feature-gating metadata.
2//!
3//! This crate owns the check table used by settings and runtime feature gates.
4
5#![forbid(unsafe_code)]
6
7use depguard_types::{Severity, ids};
8
9#[derive(Clone, Copy, Debug)]
10pub struct CheckCatalogEntry {
11    pub id: &'static str,
12    /// Canonical finding codes emitted by this check.
13    pub codes: &'static [&'static str],
14    /// Whether the check is on by default for the strict profile.
15    pub strict_enabled: bool,
16    /// Strict-profile default severity.
17    pub strict_severity: Severity,
18    /// Whether the check is on by default for the warn/compat profiles.
19    pub warn_enabled: bool,
20    /// Warn-profile default severity.
21    pub warn_severity: Severity,
22    /// Owning cargo feature gate for this check.
23    pub feature: CheckFeature,
24    /// Primary BDD feature file that should exercise this check.
25    pub bdd_feature_file: &'static str,
26}
27
28#[derive(Clone, Copy, Debug)]
29pub struct ProfileCheck {
30    pub id: &'static str,
31    pub enabled: bool,
32    pub severity: Severity,
33}
34
35#[derive(Clone, Copy, Debug)]
36pub enum CheckFeature {
37    NoWildcards,
38    PathRequiresVersion,
39    PathSafety,
40    WorkspaceInheritance,
41    GitRequiresVersion,
42    DevOnlyInNormal,
43    DefaultFeaturesExplicit,
44    NoMultipleVersions,
45    OptionalUnused,
46    YankedVersions,
47}
48
49const CHECK_CATALOG: &[CheckCatalogEntry] = &[
50    CheckCatalogEntry {
51        id: ids::CHECK_DEPS_NO_WILDCARDS,
52        codes: &[ids::CODE_WILDCARD_VERSION],
53        strict_enabled: true,
54        strict_severity: Severity::Error,
55        warn_enabled: true,
56        warn_severity: Severity::Warning,
57        feature: CheckFeature::NoWildcards,
58        bdd_feature_file: "rules_no_wildcards.feature",
59    },
60    CheckCatalogEntry {
61        id: ids::CHECK_DEPS_PATH_REQUIRES_VERSION,
62        codes: &[ids::CODE_PATH_WITHOUT_VERSION],
63        strict_enabled: true,
64        strict_severity: Severity::Error,
65        warn_enabled: true,
66        warn_severity: Severity::Warning,
67        feature: CheckFeature::PathRequiresVersion,
68        bdd_feature_file: "rules_path_requires_version.feature",
69    },
70    CheckCatalogEntry {
71        id: ids::CHECK_DEPS_PATH_SAFETY,
72        codes: &[ids::CODE_ABSOLUTE_PATH, ids::CODE_PARENT_ESCAPE],
73        strict_enabled: true,
74        strict_severity: Severity::Error,
75        warn_enabled: true,
76        warn_severity: Severity::Warning,
77        feature: CheckFeature::PathSafety,
78        bdd_feature_file: "rules_path_safety.feature",
79    },
80    CheckCatalogEntry {
81        id: ids::CHECK_DEPS_WORKSPACE_INHERITANCE,
82        codes: &[ids::CODE_MISSING_WORKSPACE_TRUE],
83        strict_enabled: false,
84        strict_severity: Severity::Error,
85        warn_enabled: false,
86        warn_severity: Severity::Warning,
87        feature: CheckFeature::WorkspaceInheritance,
88        bdd_feature_file: "rules_workspace_inheritance.feature",
89    },
90    CheckCatalogEntry {
91        id: ids::CHECK_DEPS_GIT_REQUIRES_VERSION,
92        codes: &[ids::CODE_GIT_WITHOUT_VERSION],
93        strict_enabled: false,
94        strict_severity: Severity::Error,
95        warn_enabled: false,
96        warn_severity: Severity::Warning,
97        feature: CheckFeature::GitRequiresVersion,
98        bdd_feature_file: "checks.feature",
99    },
100    CheckCatalogEntry {
101        id: ids::CHECK_DEPS_DEFAULT_FEATURES_EXPLICIT,
102        codes: &[ids::CODE_DEFAULT_FEATURES_IMPLICIT],
103        strict_enabled: false,
104        strict_severity: Severity::Warning,
105        warn_enabled: false,
106        warn_severity: Severity::Warning,
107        feature: CheckFeature::DefaultFeaturesExplicit,
108        bdd_feature_file: "checks.feature",
109    },
110    CheckCatalogEntry {
111        id: ids::CHECK_DEPS_NO_MULTIPLE_VERSIONS,
112        codes: &[ids::CODE_DUPLICATE_DIFFERENT_VERSIONS],
113        strict_enabled: false,
114        strict_severity: Severity::Warning,
115        warn_enabled: false,
116        warn_severity: Severity::Warning,
117        feature: CheckFeature::NoMultipleVersions,
118        bdd_feature_file: "checks.feature",
119    },
120    CheckCatalogEntry {
121        id: ids::CHECK_DEPS_OPTIONAL_UNUSED,
122        codes: &[ids::CODE_OPTIONAL_NOT_IN_FEATURES],
123        strict_enabled: false,
124        strict_severity: Severity::Warning,
125        warn_enabled: false,
126        warn_severity: Severity::Warning,
127        feature: CheckFeature::OptionalUnused,
128        bdd_feature_file: "checks.feature",
129    },
130    CheckCatalogEntry {
131        id: ids::CHECK_DEPS_DEV_ONLY_IN_NORMAL,
132        codes: &[ids::CODE_DEV_DEP_IN_NORMAL],
133        strict_enabled: false,
134        strict_severity: Severity::Warning,
135        warn_enabled: false,
136        warn_severity: Severity::Warning,
137        feature: CheckFeature::DevOnlyInNormal,
138        bdd_feature_file: "checks.feature",
139    },
140    CheckCatalogEntry {
141        id: ids::CHECK_DEPS_YANKED_VERSIONS,
142        codes: &[ids::CODE_VERSION_YANKED],
143        strict_enabled: false,
144        strict_severity: Severity::Error,
145        warn_enabled: false,
146        warn_severity: Severity::Error,
147        feature: CheckFeature::YankedVersions,
148        bdd_feature_file: "roadmap.feature",
149    },
150];
151
152impl CheckFeature {
153    pub const fn cargo_feature(self) -> &'static str {
154        match self {
155            Self::NoWildcards => "check-no-wildcards",
156            Self::PathRequiresVersion => "check-path-requires-version",
157            Self::PathSafety => "check-path-safety",
158            Self::WorkspaceInheritance => "check-workspace-inheritance",
159            Self::GitRequiresVersion => "check-git-requires-version",
160            Self::DevOnlyInNormal => "check-dev-only-in-normal",
161            Self::DefaultFeaturesExplicit => "check-default-features-explicit",
162            Self::NoMultipleVersions => "check-no-multiple-versions",
163            Self::OptionalUnused => "check-optional-unused",
164            Self::YankedVersions => "check-yanked-versions",
165        }
166    }
167
168    pub const fn is_enabled(self) -> bool {
169        match self {
170            Self::NoWildcards => cfg!(feature = "check-no-wildcards"),
171            Self::PathRequiresVersion => cfg!(feature = "check-path-requires-version"),
172            Self::PathSafety => cfg!(feature = "check-path-safety"),
173            Self::WorkspaceInheritance => cfg!(feature = "check-workspace-inheritance"),
174            Self::GitRequiresVersion => cfg!(feature = "check-git-requires-version"),
175            Self::DevOnlyInNormal => cfg!(feature = "check-dev-only-in-normal"),
176            Self::DefaultFeaturesExplicit => cfg!(feature = "check-default-features-explicit"),
177            Self::NoMultipleVersions => cfg!(feature = "check-no-multiple-versions"),
178            Self::OptionalUnused => cfg!(feature = "check-optional-unused"),
179            Self::YankedVersions => cfg!(feature = "check-yanked-versions"),
180        }
181    }
182}
183
184pub fn catalog() -> &'static [CheckCatalogEntry] {
185    CHECK_CATALOG
186}
187
188pub fn is_known_check_id(check_id: &str) -> bool {
189    CHECK_CATALOG.iter().any(|entry| entry.id == check_id)
190}
191
192pub fn all_check_ids() -> Vec<&'static str> {
193    CHECK_CATALOG.iter().map(|entry| entry.id).collect()
194}
195
196pub fn all_codes() -> Vec<&'static str> {
197    CHECK_CATALOG
198        .iter()
199        .flat_map(|entry| entry.codes.iter().copied())
200        .collect()
201}
202
203pub fn is_check_available(check_id: &str) -> bool {
204    entry(check_id).is_some_and(|entry| entry.feature.is_enabled())
205}
206
207pub fn entry(check_id: &str) -> Option<&'static CheckCatalogEntry> {
208    CHECK_CATALOG.iter().find(|entry| entry.id == check_id)
209}
210
211pub fn feature_name(check_id: &str) -> Option<&'static str> {
212    entry(check_id).map(|entry| entry.feature.cargo_feature())
213}
214
215pub fn bdd_feature_file(check_id: &str) -> Option<&'static str> {
216    entry(check_id).map(|entry| entry.bdd_feature_file)
217}
218
219pub fn checks_for_profile(profile: &str) -> Vec<ProfileCheck> {
220    let profile_is_warnish = matches!(profile, "warn" | "team" | "compat" | "oss");
221
222    CHECK_CATALOG
223        .iter()
224        .map(|entry| {
225            if profile_is_warnish {
226                ProfileCheck {
227                    id: entry.id,
228                    enabled: entry.warn_enabled,
229                    severity: entry.warn_severity,
230                }
231            } else {
232                ProfileCheck {
233                    id: entry.id,
234                    enabled: entry.strict_enabled,
235                    severity: entry.strict_severity,
236                }
237            }
238        })
239        .collect()
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn all_catalog_ids_have_explanations() {
248        for entry in catalog() {
249            assert!(
250                depguard_types::explain::lookup_explanation(entry.id).is_some(),
251                "check id {} has explanation",
252                entry.id
253            );
254        }
255    }
256
257    #[test]
258    fn strict_and_warn_profiles_cover_all_checks() {
259        let strict = checks_for_profile("strict");
260        let warn = checks_for_profile("warn");
261        assert_eq!(strict.len(), catalog().len());
262        assert_eq!(warn.len(), catalog().len());
263    }
264
265    #[test]
266    fn check_catalog_entries_have_bdd_feature_file() {
267        for entry in catalog() {
268            assert!(
269                !entry.bdd_feature_file.is_empty(),
270                "{} must define a BDD feature file",
271                entry.id
272            );
273        }
274    }
275
276    #[test]
277    fn check_features_default_to_enabled() {
278        for entry in catalog() {
279            assert!(
280                entry.feature.is_enabled(),
281                "{} feature should be enabled",
282                entry.id
283            );
284        }
285    }
286}