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