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};
8use crate::util::GlobalContext;
9use crate::util::context::WarningHandling;
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<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(&self, lint_level: LintLevel, source: LintLevelSource) -> String {
102        format!("`cargo::{}` is set to `{lint_level}` {source}", self.name,)
103    }
104}
105
106pub struct LintLevelProduct {
107    pub level: LintLevel,
108    pub source: LintLevelSource,
109}
110
111#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
112pub enum LintLevel {
113    Allow,
114    Warn,
115    Deny,
116    Forbid,
117}
118
119impl Display for LintLevel {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        match self {
122            LintLevel::Allow => write!(f, "allow"),
123            LintLevel::Warn => write!(f, "warn"),
124            LintLevel::Deny => write!(f, "deny"),
125            LintLevel::Forbid => write!(f, "forbid"),
126        }
127    }
128}
129
130impl LintLevel {
131    pub fn is_warn(&self) -> bool {
132        self == &LintLevel::Warn
133    }
134
135    pub fn is_error(&self) -> bool {
136        self == &LintLevel::Forbid || self == &LintLevel::Deny
137    }
138
139    pub fn to_diagnostic_level(self) -> Level<'static> {
140        match self {
141            LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
142            LintLevel::Warn => Level::WARNING,
143            LintLevel::Deny => Level::ERROR,
144            LintLevel::Forbid => Level::ERROR,
145        }
146    }
147
148    pub fn force(self) -> bool {
149        match self {
150            Self::Allow => false,
151            Self::Warn => true,
152            Self::Deny => true,
153            Self::Forbid => true,
154        }
155    }
156}
157
158impl From<manifest::TomlLintLevel> for LintLevel {
159    fn from(toml_lint_level: manifest::TomlLintLevel) -> LintLevel {
160        match toml_lint_level {
161            manifest::TomlLintLevel::Allow => LintLevel::Allow,
162            manifest::TomlLintLevel::Warn => LintLevel::Warn,
163            manifest::TomlLintLevel::Deny => LintLevel::Deny,
164            manifest::TomlLintLevel::Forbid => LintLevel::Forbid,
165        }
166    }
167}
168
169#[derive(Copy, Clone, Debug, PartialEq, Eq)]
170pub enum LintLevelSource {
171    Default,
172    Package,
173}
174
175impl Display for LintLevelSource {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        match self {
178            LintLevelSource::Default => write!(f, "by default"),
179            LintLevelSource::Package => write!(f, "in `[lints]`"),
180        }
181    }
182}
183
184impl LintLevelSource {
185    pub(crate) fn is_user_specified(&self) -> bool {
186        match self {
187            LintLevelSource::Default => false,
188            LintLevelSource::Package => true,
189        }
190    }
191}
192
193pub(crate) fn level_priority(
194    name: &str,
195    default_level: LintLevel,
196    pkg_lints: &manifest::TomlToolLints,
197) -> (LintLevel, LintLevelSource, i8) {
198    if let Some(defined_level) = pkg_lints.get(name) {
199        (
200            defined_level.level().into(),
201            LintLevelSource::Package,
202            defined_level.priority(),
203        )
204    } else {
205        (default_level, LintLevelSource::Default, 0)
206    }
207}
208
209#[derive(Clone, Debug)]
210pub struct LintGroup {
211    pub name: &'static str,
212    pub default_level: LintLevel,
213    pub desc: &'static str,
214    pub feature_gate: Option<&'static Feature>,
215    pub hidden: bool,
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    const STYLE: LintGroup = LintGroup {
223        name: "style",
224        desc: "code that should be written in a more idiomatic way",
225        default_level: LintLevel::Warn,
226        feature_gate: None,
227        hidden: false,
228    };
229
230    fn gctx() -> GlobalContext {
231        let cwd = std::env::current_dir().unwrap();
232        GlobalContext::new(
233            cargo_util_terminal::Shell::new(),
234            cwd.clone(),
235            home::cargo_home_with_cwd(&cwd).unwrap(),
236        )
237    }
238
239    fn test_lint(name: &'static str, group: &'static LintGroup) -> Lint {
240        Lint {
241            name,
242            desc: "test lint",
243            primary_group: group,
244            msrv: None,
245            feature_gate: None,
246            docs: None,
247        }
248    }
249
250    #[test]
251    fn lint_level_prefers_user_specified_over_default() {
252        let lint = test_lint("unused_dependencies", &STYLE);
253
254        let mut pkg_lints = manifest::TomlToolLints::new();
255        pkg_lints.insert(
256            "unused_dependencies".to_string(),
257            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
258        );
259        let features = Features::default();
260
261        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
262        assert_eq!(level, LintLevel::Deny);
263        assert_eq!(source, LintLevelSource::Package);
264    }
265
266    #[test]
267    fn lint_level_group_overrides_default() {
268        let lint = test_lint("non_kebab_case_bins", &STYLE);
269
270        let mut pkg_lints = manifest::TomlToolLints::new();
271        pkg_lints.insert(
272            "style".to_string(),
273            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
274        );
275        let features = Features::default();
276
277        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
278        assert_eq!(level, LintLevel::Deny);
279        assert_eq!(source, LintLevelSource::Package);
280    }
281
282    #[test]
283    fn default_group_overrides_default() {
284        let lint = test_lint("non_kebab_case_bins", &STYLE);
285
286        let mut pkg_lints = manifest::TomlToolLints::new();
287        pkg_lints.insert(
288            "default".to_string(),
289            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
290        );
291        let features = Features::default();
292
293        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
294        assert_eq!(level, LintLevel::Deny);
295        assert_eq!(source, LintLevelSource::Package);
296    }
297
298    #[test]
299    fn default_before_primary() {
300        let lint = test_lint("non_kebab_case_bins", &STYLE);
301
302        let mut pkg_lints = manifest::TomlToolLints::new();
303        pkg_lints.insert(
304            "default".to_string(),
305            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
306        );
307        pkg_lints.insert(
308            "style".to_string(),
309            manifest::TomlLint::Level(manifest::TomlLintLevel::Allow),
310        );
311        let features = Features::default();
312
313        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
314        assert_eq!(level, LintLevel::Deny);
315        assert_eq!(source, LintLevelSource::Package);
316    }
317
318    #[test]
319    fn default_after_primary() {
320        let lint = test_lint("non_kebab_case_bins", &STYLE);
321
322        let mut pkg_lints = manifest::TomlToolLints::new();
323        pkg_lints.insert(
324            "style".to_string(),
325            manifest::TomlLint::Level(manifest::TomlLintLevel::Allow),
326        );
327        pkg_lints.insert(
328            "default".to_string(),
329            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
330        );
331        let features = Features::default();
332
333        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
334        assert_eq!(level, LintLevel::Deny);
335        assert_eq!(source, LintLevelSource::Package);
336    }
337
338    #[test]
339    fn default_higher_than_primary() {
340        let lint = test_lint("non_kebab_case_bins", &STYLE);
341
342        let mut pkg_lints = manifest::TomlToolLints::new();
343        pkg_lints.insert(
344            "default".to_string(),
345            manifest::TomlLint::Config(manifest::TomlLintConfig {
346                level: manifest::TomlLintLevel::Deny,
347                priority: 1,
348                config: Default::default(),
349            }),
350        );
351        pkg_lints.insert(
352            "style".to_string(),
353            manifest::TomlLint::Config(manifest::TomlLintConfig {
354                level: manifest::TomlLintLevel::Allow,
355                priority: -1,
356                config: Default::default(),
357            }),
358        );
359        let features = Features::default();
360
361        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
362        assert_eq!(level, LintLevel::Deny);
363        assert_eq!(source, LintLevelSource::Package);
364    }
365
366    #[test]
367    fn default_lower_than_primary() {
368        let lint = test_lint("non_kebab_case_bins", &STYLE);
369
370        let mut pkg_lints = manifest::TomlToolLints::new();
371        pkg_lints.insert(
372            "default".to_string(),
373            manifest::TomlLint::Config(manifest::TomlLintConfig {
374                level: manifest::TomlLintLevel::Deny,
375                priority: -1,
376                config: Default::default(),
377            }),
378        );
379        pkg_lints.insert(
380            "style".to_string(),
381            manifest::TomlLint::Config(manifest::TomlLintConfig {
382                level: manifest::TomlLintLevel::Allow,
383                priority: 1,
384                config: Default::default(),
385            }),
386        );
387        let features = Features::default();
388
389        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
390        assert_eq!(level, LintLevel::Allow);
391        assert_eq!(source, LintLevelSource::Package);
392    }
393}