Skip to main content

cargo/diagnostics/
lint.rs

1use std::cmp::{Reverse, max_by_key};
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 (_, (level, source, _)) = max_by_key(
64            (self.name, lint_level_priority),
65            (self.primary_group.name, group_level_priority),
66            |(n, (l, s, p))| {
67                (
68                    l == &LintLevel::Forbid,
69                    *s != LintLevelSource::Default,
70                    *p,
71                    Reverse(*n),
72                )
73            },
74        );
75        LintLevelProduct { level, source }
76    }
77
78    pub fn emitted_source(&self, lint_level: LintLevel, source: LintLevelSource) -> String {
79        format!("`cargo::{}` is set to `{lint_level}` {source}", self.name,)
80    }
81}
82
83pub struct LintLevelProduct {
84    pub level: LintLevel,
85    pub source: LintLevelSource,
86}
87
88#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
89pub enum LintLevel {
90    Allow,
91    Warn,
92    Deny,
93    Forbid,
94}
95
96impl Display for LintLevel {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        match self {
99            LintLevel::Allow => write!(f, "allow"),
100            LintLevel::Warn => write!(f, "warn"),
101            LintLevel::Deny => write!(f, "deny"),
102            LintLevel::Forbid => write!(f, "forbid"),
103        }
104    }
105}
106
107impl LintLevel {
108    pub fn is_warn(&self) -> bool {
109        self == &LintLevel::Warn
110    }
111
112    pub fn is_error(&self) -> bool {
113        self == &LintLevel::Forbid || self == &LintLevel::Deny
114    }
115
116    pub fn to_diagnostic_level(self) -> Level<'static> {
117        match self {
118            LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
119            LintLevel::Warn => Level::WARNING,
120            LintLevel::Deny => Level::ERROR,
121            LintLevel::Forbid => Level::ERROR,
122        }
123    }
124
125    pub fn force(self) -> bool {
126        match self {
127            Self::Allow => false,
128            Self::Warn => true,
129            Self::Deny => true,
130            Self::Forbid => true,
131        }
132    }
133}
134
135impl From<manifest::TomlLintLevel> for LintLevel {
136    fn from(toml_lint_level: manifest::TomlLintLevel) -> LintLevel {
137        match toml_lint_level {
138            manifest::TomlLintLevel::Allow => LintLevel::Allow,
139            manifest::TomlLintLevel::Warn => LintLevel::Warn,
140            manifest::TomlLintLevel::Deny => LintLevel::Deny,
141            manifest::TomlLintLevel::Forbid => LintLevel::Forbid,
142        }
143    }
144}
145
146#[derive(Copy, Clone, Debug, PartialEq, Eq)]
147pub enum LintLevelSource {
148    Default,
149    Package,
150}
151
152impl Display for LintLevelSource {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        match self {
155            LintLevelSource::Default => write!(f, "by default"),
156            LintLevelSource::Package => write!(f, "in `[lints]`"),
157        }
158    }
159}
160
161impl LintLevelSource {
162    pub(crate) fn is_user_specified(&self) -> bool {
163        match self {
164            LintLevelSource::Default => false,
165            LintLevelSource::Package => true,
166        }
167    }
168}
169
170pub(crate) fn level_priority(
171    name: &str,
172    default_level: LintLevel,
173    pkg_lints: &manifest::TomlToolLints,
174) -> (LintLevel, LintLevelSource, i8) {
175    if let Some(defined_level) = pkg_lints.get(name) {
176        (
177            defined_level.level().into(),
178            LintLevelSource::Package,
179            defined_level.priority(),
180        )
181    } else {
182        (default_level, LintLevelSource::Default, 0)
183    }
184}
185
186#[derive(Clone, Debug)]
187pub struct LintGroup {
188    pub name: &'static str,
189    pub default_level: LintLevel,
190    pub desc: &'static str,
191    pub feature_gate: Option<&'static Feature>,
192    pub hidden: bool,
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    const STYLE: LintGroup = LintGroup {
200        name: "style",
201        desc: "code that should be written in a more idiomatic way",
202        default_level: LintLevel::Warn,
203        feature_gate: None,
204        hidden: false,
205    };
206
207    fn test_lint(name: &'static str, group: &'static LintGroup) -> Lint {
208        Lint {
209            name,
210            desc: "test lint",
211            primary_group: group,
212            msrv: None,
213            feature_gate: None,
214            docs: None,
215        }
216    }
217
218    #[test]
219    fn lint_level_prefers_user_specified_over_default() {
220        let lint = test_lint("unused_dependencies", &STYLE);
221
222        let mut pkg_lints = manifest::TomlToolLints::new();
223        pkg_lints.insert(
224            "unused_dependencies".to_string(),
225            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
226        );
227        let features = Features::default();
228
229        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features);
230        assert_eq!(level, LintLevel::Deny);
231        assert_eq!(source, LintLevelSource::Package);
232    }
233
234    #[test]
235    fn lint_level_group_overrides_default() {
236        let lint = test_lint("non_kebab_case_bins", &STYLE);
237
238        let mut pkg_lints = manifest::TomlToolLints::new();
239        pkg_lints.insert(
240            "style".to_string(),
241            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
242        );
243        let features = Features::default();
244
245        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features);
246        assert_eq!(level, LintLevel::Deny);
247        assert_eq!(source, LintLevelSource::Package);
248    }
249}