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