Skip to main content

cargo/diagnostics/
lint.rs

1use std::cmp::Reverse;
2use std::fmt::Display;
3
4use cargo_util_schemas::manifest;
5use cargo_util_terminal::report::Level;
6
7use crate::context::WarningHandling;
8use crate::util::GlobalContext;
9use crate::workspace::{Feature, Features};
10
11#[derive(Clone, Debug)]
12pub struct Lint {
13    pub name: &'static str,
14    pub primary_group: &'static LintGroup,
15    /// The minimum supported Rust version for applying this lint
16    ///
17    /// Note: If the lint is on by default and did not qualify as a hard-warning before the
18    /// linting system, then at earliest an MSRV of 1.78 is required as `[lints.cargo]` was a hard
19    /// error before then.
20    pub msrv: Option<manifest::RustVersion>,
21    pub feature_gate: Option<&'static Feature>,
22    /// This is a markdown formatted string that will be used when generating
23    /// the lint documentation. If docs is `None`, the lint will not be
24    /// documented.
25    pub docs: Option<&'static str>,
26}
27
28impl Lint {
29    pub fn level(
30        &self,
31        pkg_lints: &manifest::TomlToolLints,
32        pkg_rust_version: Option<&manifest::RustVersion>,
33        unstable_features: &Features,
34        gctx: &GlobalContext,
35    ) -> LintLevelProduct {
36        // We should return `Allow` if a lint is behind a feature, but it is
37        // not enabled, that way the lint does not run.
38        if self
39            .feature_gate
40            .is_some_and(|f| !unstable_features.is_enabled(f))
41        {
42            let level = LintLevel::Allow;
43            let source = LintLevelSource::Default;
44            return LintLevelProduct { level, source };
45        }
46
47        if let (Some(msrv), Some(pkg_rust_version)) = (&self.msrv, pkg_rust_version) {
48            let pkg_rust_version = pkg_rust_version.to_partial();
49            if !msrv.is_compatible_with(&pkg_rust_version) {
50                let level = LintLevel::Allow;
51                let source = LintLevelSource::Default;
52                return LintLevelProduct { level, source };
53            }
54        }
55
56        let lint_level_priority =
57            level_priority(self.name, self.primary_group.default_level, pkg_lints);
58
59        let group_level_priority = level_priority(
60            self.primary_group.name,
61            self.primary_group.default_level,
62            pkg_lints,
63        );
64
65        let default_group = if LintLevel::Warn <= self.primary_group.default_level {
66            let lint_level_priority =
67                level_priority("default", self.primary_group.default_level, pkg_lints);
68            Some(("default", lint_level_priority))
69        } else {
70            None
71        };
72
73        let (_, (level, source, _)) = [
74            (self.name, lint_level_priority),
75            (self.primary_group.name, group_level_priority),
76        ]
77        .into_iter()
78        .chain(default_group)
79        .max_by_key(|(n, (l, s, p))| {
80            (
81                l == &LintLevel::Forbid,
82                *s != LintLevelSource::Default,
83                *p,
84                Reverse(*n),
85            )
86        })
87        .unwrap();
88
89        let (level, source) = match (level, gctx.warning_handling().ok()) {
90            // `Deny` needs to be handled later, at the end of the operation
91            (LintLevel::Warn, Some(WarningHandling::Allow)) => {
92                (LintLevel::Allow, LintLevelSource::Default)
93            }
94            _ => (level, source),
95        };
96
97        LintLevelProduct { level, source }
98    }
99
100    pub fn emitted_source(&self, lint_level: LintLevel, source: LintLevelSource) -> String {
101        format!("`cargo::{}` is set to `{lint_level}` {source}", self.name,)
102    }
103}
104
105pub struct LintLevelProduct {
106    pub level: LintLevel,
107    pub source: LintLevelSource,
108}
109
110#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
111pub enum LintLevel {
112    Allow,
113    Warn,
114    Deny,
115    Forbid,
116}
117
118impl Display for LintLevel {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        match self {
121            LintLevel::Allow => write!(f, "allow"),
122            LintLevel::Warn => write!(f, "warn"),
123            LintLevel::Deny => write!(f, "deny"),
124            LintLevel::Forbid => write!(f, "forbid"),
125        }
126    }
127}
128
129impl LintLevel {
130    pub fn is_warn(&self) -> bool {
131        self == &LintLevel::Warn
132    }
133
134    pub fn is_error(&self) -> bool {
135        self == &LintLevel::Forbid || self == &LintLevel::Deny
136    }
137
138    pub fn to_diagnostic_level(self) -> Level<'static> {
139        match self {
140            LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
141            LintLevel::Warn => Level::WARNING,
142            LintLevel::Deny => Level::ERROR,
143            LintLevel::Forbid => Level::ERROR,
144        }
145    }
146
147    pub fn force(self) -> bool {
148        match self {
149            Self::Allow => false,
150            Self::Warn => true,
151            Self::Deny => true,
152            Self::Forbid => true,
153        }
154    }
155}
156
157impl From<manifest::TomlLintLevel> for LintLevel {
158    fn from(toml_lint_level: manifest::TomlLintLevel) -> LintLevel {
159        match toml_lint_level {
160            manifest::TomlLintLevel::Allow => LintLevel::Allow,
161            manifest::TomlLintLevel::Warn => LintLevel::Warn,
162            manifest::TomlLintLevel::Deny => LintLevel::Deny,
163            manifest::TomlLintLevel::Forbid => LintLevel::Forbid,
164        }
165    }
166}
167
168#[derive(Copy, Clone, Debug, PartialEq, Eq)]
169pub enum LintLevelSource {
170    Default,
171    Package,
172}
173
174impl Display for LintLevelSource {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        match self {
177            LintLevelSource::Default => write!(f, "by default"),
178            LintLevelSource::Package => write!(f, "in `[lints]`"),
179        }
180    }
181}
182
183impl LintLevelSource {
184    pub(crate) fn is_user_specified(&self) -> bool {
185        match self {
186            LintLevelSource::Default => false,
187            LintLevelSource::Package => true,
188        }
189    }
190}
191
192pub(crate) fn level_priority(
193    name: &str,
194    default_level: LintLevel,
195    pkg_lints: &manifest::TomlToolLints,
196) -> (LintLevel, LintLevelSource, i8) {
197    if let Some(defined_level) = pkg_lints.get(name) {
198        (
199            defined_level.level().into(),
200            LintLevelSource::Package,
201            defined_level.priority(),
202        )
203    } else {
204        (default_level, LintLevelSource::Default, 0)
205    }
206}
207
208#[derive(Clone, Debug)]
209pub struct LintGroup {
210    pub name: &'static str,
211    pub default_level: LintLevel,
212    pub desc: &'static str,
213    pub feature_gate: Option<&'static Feature>,
214    pub hidden: bool,
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    const STYLE: LintGroup = LintGroup {
222        name: "style",
223        desc: "code that should be written in a more idiomatic way",
224        default_level: LintLevel::Warn,
225        feature_gate: None,
226        hidden: false,
227    };
228
229    fn gctx() -> GlobalContext {
230        let cwd = std::env::current_dir().unwrap();
231        GlobalContext::new(
232            cargo_util_terminal::Shell::new(),
233            cwd.clone(),
234            home::cargo_home_with_cwd(&cwd).unwrap(),
235        )
236    }
237
238    fn test_lint(name: &'static str, group: &'static LintGroup) -> Lint {
239        Lint {
240            name,
241            primary_group: group,
242            msrv: None,
243            feature_gate: None,
244            docs: None,
245        }
246    }
247
248    #[test]
249    fn lint_level_prefers_user_specified_over_default() {
250        let lint = test_lint("unused_dependencies", &STYLE);
251
252        let mut pkg_lints = manifest::TomlToolLints::new();
253        pkg_lints.insert(
254            "unused_dependencies".to_string(),
255            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
256        );
257        let features = Features::default();
258
259        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
260        assert_eq!(level, LintLevel::Deny);
261        assert_eq!(source, LintLevelSource::Package);
262    }
263
264    #[test]
265    fn lint_level_group_overrides_default() {
266        let lint = test_lint("non_kebab_case_bins", &STYLE);
267
268        let mut pkg_lints = manifest::TomlToolLints::new();
269        pkg_lints.insert(
270            "style".to_string(),
271            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
272        );
273        let features = Features::default();
274
275        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
276        assert_eq!(level, LintLevel::Deny);
277        assert_eq!(source, LintLevelSource::Package);
278    }
279
280    #[test]
281    fn default_group_overrides_default() {
282        let lint = test_lint("non_kebab_case_bins", &STYLE);
283
284        let mut pkg_lints = manifest::TomlToolLints::new();
285        pkg_lints.insert(
286            "default".to_string(),
287            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
288        );
289        let features = Features::default();
290
291        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
292        assert_eq!(level, LintLevel::Deny);
293        assert_eq!(source, LintLevelSource::Package);
294    }
295
296    #[test]
297    fn default_before_primary() {
298        let lint = test_lint("non_kebab_case_bins", &STYLE);
299
300        let mut pkg_lints = manifest::TomlToolLints::new();
301        pkg_lints.insert(
302            "default".to_string(),
303            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
304        );
305        pkg_lints.insert(
306            "style".to_string(),
307            manifest::TomlLint::Level(manifest::TomlLintLevel::Allow),
308        );
309        let features = Features::default();
310
311        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
312        assert_eq!(level, LintLevel::Deny);
313        assert_eq!(source, LintLevelSource::Package);
314    }
315
316    #[test]
317    fn default_after_primary() {
318        let lint = test_lint("non_kebab_case_bins", &STYLE);
319
320        let mut pkg_lints = manifest::TomlToolLints::new();
321        pkg_lints.insert(
322            "style".to_string(),
323            manifest::TomlLint::Level(manifest::TomlLintLevel::Allow),
324        );
325        pkg_lints.insert(
326            "default".to_string(),
327            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
328        );
329        let features = Features::default();
330
331        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
332        assert_eq!(level, LintLevel::Deny);
333        assert_eq!(source, LintLevelSource::Package);
334    }
335
336    #[test]
337    fn default_higher_than_primary() {
338        let lint = test_lint("non_kebab_case_bins", &STYLE);
339
340        let mut pkg_lints = manifest::TomlToolLints::new();
341        pkg_lints.insert(
342            "default".to_string(),
343            manifest::TomlLint::Config(manifest::TomlLintConfig {
344                level: manifest::TomlLintLevel::Deny,
345                priority: 1,
346                config: Default::default(),
347            }),
348        );
349        pkg_lints.insert(
350            "style".to_string(),
351            manifest::TomlLint::Config(manifest::TomlLintConfig {
352                level: manifest::TomlLintLevel::Allow,
353                priority: -1,
354                config: Default::default(),
355            }),
356        );
357        let features = Features::default();
358
359        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
360        assert_eq!(level, LintLevel::Deny);
361        assert_eq!(source, LintLevelSource::Package);
362    }
363
364    #[test]
365    fn default_lower_than_primary() {
366        let lint = test_lint("non_kebab_case_bins", &STYLE);
367
368        let mut pkg_lints = manifest::TomlToolLints::new();
369        pkg_lints.insert(
370            "default".to_string(),
371            manifest::TomlLint::Config(manifest::TomlLintConfig {
372                level: manifest::TomlLintLevel::Deny,
373                priority: -1,
374                config: Default::default(),
375            }),
376        );
377        pkg_lints.insert(
378            "style".to_string(),
379            manifest::TomlLint::Config(manifest::TomlLintConfig {
380                level: manifest::TomlLintLevel::Allow,
381                priority: 1,
382                config: Default::default(),
383            }),
384        );
385        let features = Features::default();
386
387        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
388        assert_eq!(level, LintLevel::Allow);
389        assert_eq!(source, LintLevelSource::Package);
390    }
391}