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