Skip to main content

cargo/lints/
mod.rs

1use std::borrow::Cow;
2use std::cmp::{Reverse, max_by_key};
3use std::fmt::Display;
4use std::ops::Range;
5use std::path::Path;
6
7use cargo_util_schemas::manifest::RustVersion;
8use cargo_util_schemas::manifest::TomlLintLevel;
9use cargo_util_schemas::manifest::TomlToolLints;
10use cargo_util_terminal::report::AnnotationKind;
11use cargo_util_terminal::report::Group;
12use cargo_util_terminal::report::Level;
13use cargo_util_terminal::report::Snippet;
14use pathdiff::diff_paths;
15
16use crate::core::Workspace;
17use crate::core::{Edition, Feature, Features, MaybePackage, Package};
18use crate::{CargoResult, GlobalContext};
19
20pub mod rules;
21pub use rules::LINTS;
22
23pub static LINT_GROUPS: &[LintGroup] = &[
24    COMPLEXITY,
25    CORRECTNESS,
26    NURSERY,
27    PEDANTIC,
28    PERF,
29    RESTRICTION,
30    STYLE,
31    SUSPICIOUS,
32    TEST_DUMMY_UNSTABLE,
33];
34
35/// Scope at which a lint runs: package-level or workspace-level.
36pub enum ManifestFor<'a> {
37    /// Lint runs for a specific package.
38    Package(&'a Package),
39    /// Lint runs for workspace-level config.
40    Workspace {
41        ws: &'a Workspace<'a>,
42        maybe_pkg: &'a MaybePackage,
43    },
44}
45
46impl ManifestFor<'_> {
47    fn lint_level(&self, pkg_lints: &TomlToolLints, lint: &Lint) -> (LintLevel, LintLevelReason) {
48        lint.level(pkg_lints, self.rust_version(), self.unstable_features())
49    }
50
51    pub fn rust_version(&self) -> Option<&RustVersion> {
52        match self {
53            ManifestFor::Package(p) => p.rust_version(),
54            ManifestFor::Workspace { ws, maybe_pkg: _ } => ws.lowest_rust_version(),
55        }
56    }
57
58    pub fn contents(&self) -> Option<&str> {
59        match self {
60            ManifestFor::Package(p) => p.manifest().contents(),
61            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.contents(),
62        }
63    }
64
65    pub fn document(&self) -> Option<&toml::Spanned<toml::de::DeTable<'static>>> {
66        match self {
67            ManifestFor::Package(p) => p.manifest().document(),
68            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.document(),
69        }
70    }
71
72    pub fn edition(&self) -> Edition {
73        match self {
74            ManifestFor::Package(p) => p.manifest().edition(),
75            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.edition(),
76        }
77    }
78
79    pub fn unstable_features(&self) -> &Features {
80        match self {
81            ManifestFor::Package(p) => p.manifest().unstable_features(),
82            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.unstable_features(),
83        }
84    }
85}
86
87impl<'a> From<&'a Package> for ManifestFor<'a> {
88    fn from(value: &'a Package) -> ManifestFor<'a> {
89        ManifestFor::Package(value)
90    }
91}
92
93impl<'a> From<(&'a Workspace<'a>, &'a MaybePackage)> for ManifestFor<'a> {
94    fn from((ws, maybe_pkg): (&'a Workspace<'a>, &'a MaybePackage)) -> ManifestFor<'a> {
95        ManifestFor::Workspace { ws, maybe_pkg }
96    }
97}
98
99pub fn analyze_cargo_lints_table(
100    manifest: ManifestFor<'_>,
101    manifest_path: &Path,
102    cargo_lints: &TomlToolLints,
103    error_count: &mut usize,
104    gctx: &GlobalContext,
105) -> CargoResult<()> {
106    let manifest_path = rel_cwd_manifest_path(manifest_path, gctx);
107    let mut unknown_lints = Vec::new();
108    for lint_name in cargo_lints.keys().map(|name| name) {
109        let Some((name, default_level, feature_gate)) = find_lint_or_group(lint_name) else {
110            unknown_lints.push(lint_name);
111            continue;
112        };
113
114        let (_, reason, _) = level_priority(name, *default_level, cargo_lints);
115
116        // Only run analysis on user-specified lints
117        if !reason.is_user_specified() {
118            continue;
119        }
120
121        // Only run this on lints that are gated by a feature
122        if let Some(feature_gate) = feature_gate
123            && !manifest.unstable_features().is_enabled(feature_gate)
124        {
125            report_feature_not_enabled(
126                name,
127                feature_gate,
128                &manifest,
129                &manifest_path,
130                error_count,
131                gctx,
132            )?;
133        }
134    }
135
136    rules::output_unknown_lints(
137        unknown_lints,
138        &manifest,
139        &manifest_path,
140        cargo_lints,
141        error_count,
142        gctx,
143    )?;
144
145    Ok(())
146}
147
148fn find_lint_or_group<'a>(
149    name: &str,
150) -> Option<(&'static str, &LintLevel, &Option<&'static Feature>)> {
151    if let Some(lint) = LINTS.iter().find(|l| l.name == name) {
152        Some((
153            lint.name,
154            &lint.primary_group.default_level,
155            &lint.feature_gate,
156        ))
157    } else if let Some(group) = LINT_GROUPS.iter().find(|g| g.name == name) {
158        Some((group.name, &group.default_level, &group.feature_gate))
159    } else {
160        None
161    }
162}
163
164fn report_feature_not_enabled(
165    lint_name: &str,
166    feature_gate: &Feature,
167    manifest: &ManifestFor<'_>,
168    manifest_path: &str,
169    error_count: &mut usize,
170    gctx: &GlobalContext,
171) -> CargoResult<()> {
172    let dash_feature_name = feature_gate.name().replace("_", "-");
173    let title = format!("use of unstable lint `{}`", lint_name);
174    let label = format!(
175        "this is behind `{}`, which is not enabled",
176        dash_feature_name
177    );
178    let help = format!(
179        "consider adding `cargo-features = [\"{}\"]` to the top of the manifest",
180        dash_feature_name
181    );
182
183    let key_path = match manifest {
184        ManifestFor::Package(_) => &["lints", "cargo", lint_name][..],
185        ManifestFor::Workspace { .. } => &["workspace", "lints", "cargo", lint_name][..],
186    };
187
188    let mut error = Group::with_title(Level::ERROR.primary_title(title));
189
190    if let Some(document) = manifest.document()
191        && let Some(contents) = manifest.contents()
192    {
193        let Some(span) = get_key_value_span(document, key_path) else {
194            // This lint is handled by either package or workspace lint.
195            return Ok(());
196        };
197
198        error = error.element(
199            Snippet::source(contents)
200                .path(manifest_path)
201                .annotation(AnnotationKind::Primary.span(span.key).label(label)),
202        )
203    }
204
205    let report = [error.element(Level::HELP.message(help))];
206
207    *error_count += 1;
208    gctx.shell().print_report(&report, true)?;
209
210    Ok(())
211}
212
213#[derive(Clone)]
214pub struct TomlSpan {
215    pub key: Range<usize>,
216    pub value: Range<usize>,
217}
218
219#[derive(Copy, Clone)]
220pub enum TomlIndex<'i> {
221    Key(&'i str),
222    Offset(usize),
223}
224
225impl<'i> TomlIndex<'i> {
226    fn as_key(&self) -> Option<&'i str> {
227        match self {
228            TomlIndex::Key(key) => Some(key),
229            TomlIndex::Offset(_) => None,
230        }
231    }
232}
233
234pub trait AsIndex {
235    fn as_index<'i>(&'i self) -> TomlIndex<'i>;
236}
237
238impl AsIndex for TomlIndex<'_> {
239    fn as_index<'i>(&'i self) -> TomlIndex<'i> {
240        match self {
241            TomlIndex::Key(key) => TomlIndex::Key(key),
242            TomlIndex::Offset(offset) => TomlIndex::Offset(*offset),
243        }
244    }
245}
246
247impl AsIndex for &str {
248    fn as_index<'i>(&'i self) -> TomlIndex<'i> {
249        TomlIndex::Key(self)
250    }
251}
252
253impl AsIndex for String {
254    fn as_index<'i>(&'i self) -> TomlIndex<'i> {
255        TomlIndex::Key(self.as_str())
256    }
257}
258
259impl AsIndex for usize {
260    fn as_index<'i>(&'i self) -> TomlIndex<'i> {
261        TomlIndex::Offset(*self)
262    }
263}
264
265pub fn get_key_value<'doc, 'i>(
266    document: &'doc toml::Spanned<toml::de::DeTable<'static>>,
267    path: &[impl AsIndex],
268) -> Option<(
269    &'doc toml::Spanned<Cow<'doc, str>>,
270    &'doc toml::Spanned<toml::de::DeValue<'static>>,
271)> {
272    let table = document.get_ref();
273    let mut iter = path.into_iter();
274    let index0 = iter.next()?.as_index();
275    let key0 = index0.as_key()?;
276    let (mut current_key, mut current_item) = table.get_key_value(key0)?;
277
278    while let Some(index) = iter.next() {
279        match index.as_index() {
280            TomlIndex::Key(key) => {
281                if let Some(table) = current_item.get_ref().as_table() {
282                    (current_key, current_item) = table.get_key_value(key)?;
283                } else if let Some(array) = current_item.get_ref().as_array() {
284                    current_item = array.iter().find(|item| match item.get_ref() {
285                        toml::de::DeValue::String(s) => s == key,
286                        _ => false,
287                    })?;
288                } else {
289                    return None;
290                }
291            }
292            TomlIndex::Offset(offset) => {
293                let array = current_item.get_ref().as_array()?;
294                current_item = array.get(offset)?;
295            }
296        }
297    }
298    Some((current_key, current_item))
299}
300
301pub fn get_key_value_span<'i>(
302    document: &toml::Spanned<toml::de::DeTable<'static>>,
303    path: &[impl AsIndex],
304) -> Option<TomlSpan> {
305    get_key_value(document, path).map(|(k, v)| TomlSpan {
306        key: k.span(),
307        value: v.span(),
308    })
309}
310
311/// Gets the relative path to a manifest from the current working directory, or
312/// the absolute path of the manifest if a relative path cannot be constructed
313pub fn rel_cwd_manifest_path(path: &Path, gctx: &GlobalContext) -> String {
314    diff_paths(path, gctx.cwd())
315        .unwrap_or_else(|| path.to_path_buf())
316        .display()
317        .to_string()
318}
319
320#[derive(Clone, Debug)]
321pub struct LintGroup {
322    pub name: &'static str,
323    pub default_level: LintLevel,
324    pub desc: &'static str,
325    pub feature_gate: Option<&'static Feature>,
326    pub hidden: bool,
327}
328
329const COMPLEXITY: LintGroup = LintGroup {
330    name: "complexity",
331    desc: "code that does something simple but in a complex way",
332    default_level: LintLevel::Warn,
333    feature_gate: None,
334    hidden: false,
335};
336
337const CORRECTNESS: LintGroup = LintGroup {
338    name: "correctness",
339    desc: "code that is outright wrong or useless",
340    default_level: LintLevel::Deny,
341    feature_gate: None,
342    hidden: false,
343};
344
345const NURSERY: LintGroup = LintGroup {
346    name: "nursery",
347    desc: "new lints that are still under development",
348    default_level: LintLevel::Allow,
349    feature_gate: None,
350    hidden: false,
351};
352
353const PEDANTIC: LintGroup = LintGroup {
354    name: "pedantic",
355    desc: "lints which are rather strict or have occasional false positives",
356    default_level: LintLevel::Allow,
357    feature_gate: None,
358    hidden: false,
359};
360
361const PERF: LintGroup = LintGroup {
362    name: "perf",
363    desc: "code that can be written to run faster",
364    default_level: LintLevel::Warn,
365    feature_gate: None,
366    hidden: false,
367};
368
369const RESTRICTION: LintGroup = LintGroup {
370    name: "restriction",
371    desc: "lints which prevent the use of Cargo features",
372    default_level: LintLevel::Allow,
373    feature_gate: None,
374    hidden: false,
375};
376
377const STYLE: LintGroup = LintGroup {
378    name: "style",
379    desc: "code that should be written in a more idiomatic way",
380    default_level: LintLevel::Warn,
381    feature_gate: None,
382    hidden: false,
383};
384
385const SUSPICIOUS: LintGroup = LintGroup {
386    name: "suspicious",
387    desc: "code that is most likely wrong or useless",
388    default_level: LintLevel::Warn,
389    feature_gate: None,
390    hidden: false,
391};
392
393/// This lint group is only to be used for testing purposes
394const TEST_DUMMY_UNSTABLE: LintGroup = LintGroup {
395    name: "test_dummy_unstable",
396    desc: "test_dummy_unstable is meant to only be used in tests",
397    default_level: LintLevel::Allow,
398    feature_gate: Some(Feature::test_dummy_unstable()),
399    hidden: true,
400};
401
402#[derive(Clone, Debug)]
403pub struct Lint {
404    pub name: &'static str,
405    pub desc: &'static str,
406    pub primary_group: &'static LintGroup,
407    /// The minimum supported Rust version for applying this lint
408    ///
409    /// Note: If the lint is on by default and did not qualify as a hard-warning before the
410    /// linting system, then at earliest an MSRV of 1.78 is required as `[lints.cargo]` was a hard
411    /// error before then.
412    pub msrv: Option<RustVersion>,
413    pub feature_gate: Option<&'static Feature>,
414    /// This is a markdown formatted string that will be used when generating
415    /// the lint documentation. If docs is `None`, the lint will not be
416    /// documented.
417    pub docs: Option<&'static str>,
418}
419
420impl Lint {
421    pub fn level(
422        &self,
423        pkg_lints: &TomlToolLints,
424        pkg_rust_version: Option<&RustVersion>,
425        unstable_features: &Features,
426    ) -> (LintLevel, LintLevelReason) {
427        // We should return `Allow` if a lint is behind a feature, but it is
428        // not enabled, that way the lint does not run.
429        if self
430            .feature_gate
431            .is_some_and(|f| !unstable_features.is_enabled(f))
432        {
433            return (LintLevel::Allow, LintLevelReason::Default);
434        }
435
436        if let (Some(msrv), Some(pkg_rust_version)) = (&self.msrv, pkg_rust_version) {
437            let pkg_rust_version = pkg_rust_version.to_partial();
438            if !msrv.is_compatible_with(&pkg_rust_version) {
439                return (LintLevel::Allow, LintLevelReason::Default);
440            }
441        }
442
443        let lint_level_priority =
444            level_priority(self.name, self.primary_group.default_level, pkg_lints);
445
446        let group_level_priority = level_priority(
447            self.primary_group.name,
448            self.primary_group.default_level,
449            pkg_lints,
450        );
451
452        let (_, (l, r, _)) = max_by_key(
453            (self.name, lint_level_priority),
454            (self.primary_group.name, group_level_priority),
455            |(n, (l, _, p))| (l == &LintLevel::Forbid, *p, Reverse(*n)),
456        );
457        (l, r)
458    }
459
460    pub fn emitted_source(&self, lint_level: LintLevel, reason: LintLevelReason) -> String {
461        format!("`cargo::{}` is set to `{lint_level}` {reason}", self.name,)
462    }
463}
464
465#[derive(Copy, Clone, Debug, PartialEq)]
466pub enum LintLevel {
467    Allow,
468    Warn,
469    Deny,
470    Forbid,
471}
472
473impl Display for LintLevel {
474    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475        match self {
476            LintLevel::Allow => write!(f, "allow"),
477            LintLevel::Warn => write!(f, "warn"),
478            LintLevel::Deny => write!(f, "deny"),
479            LintLevel::Forbid => write!(f, "forbid"),
480        }
481    }
482}
483
484impl LintLevel {
485    pub fn is_warn(&self) -> bool {
486        self == &LintLevel::Warn
487    }
488
489    pub fn is_error(&self) -> bool {
490        self == &LintLevel::Forbid || self == &LintLevel::Deny
491    }
492
493    pub fn to_diagnostic_level(self) -> Level<'static> {
494        match self {
495            LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
496            LintLevel::Warn => Level::WARNING,
497            LintLevel::Deny => Level::ERROR,
498            LintLevel::Forbid => Level::ERROR,
499        }
500    }
501
502    pub fn force(self) -> bool {
503        match self {
504            Self::Allow => false,
505            Self::Warn => true,
506            Self::Deny => true,
507            Self::Forbid => true,
508        }
509    }
510}
511
512impl From<TomlLintLevel> for LintLevel {
513    fn from(toml_lint_level: TomlLintLevel) -> LintLevel {
514        match toml_lint_level {
515            TomlLintLevel::Allow => LintLevel::Allow,
516            TomlLintLevel::Warn => LintLevel::Warn,
517            TomlLintLevel::Deny => LintLevel::Deny,
518            TomlLintLevel::Forbid => LintLevel::Forbid,
519        }
520    }
521}
522
523#[derive(Copy, Clone, Debug, PartialEq, Eq)]
524pub enum LintLevelReason {
525    Default,
526    Package,
527}
528
529impl Display for LintLevelReason {
530    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
531        match self {
532            LintLevelReason::Default => write!(f, "by default"),
533            LintLevelReason::Package => write!(f, "in `[lints]`"),
534        }
535    }
536}
537
538impl LintLevelReason {
539    fn is_user_specified(&self) -> bool {
540        match self {
541            LintLevelReason::Default => false,
542            LintLevelReason::Package => true,
543        }
544    }
545}
546
547fn level_priority(
548    name: &str,
549    default_level: LintLevel,
550    pkg_lints: &TomlToolLints,
551) -> (LintLevel, LintLevelReason, i8) {
552    if let Some(defined_level) = pkg_lints.get(name) {
553        (
554            defined_level.level().into(),
555            LintLevelReason::Package,
556            defined_level.priority(),
557        )
558    } else {
559        (default_level, LintLevelReason::Default, 0)
560    }
561}
562
563#[cfg(test)]
564mod tests {
565    use itertools::Itertools;
566    use snapbox::ToDebug;
567    use std::collections::HashSet;
568
569    #[test]
570    fn ensure_lint_groups_do_not_default_to_forbid() {
571        let forbid_groups = super::LINT_GROUPS
572            .iter()
573            .filter(|g| matches!(g.default_level, super::LintLevel::Forbid))
574            .collect::<Vec<_>>();
575
576        assert!(
577            forbid_groups.is_empty(),
578            "\n`LintGroup`s should never default to `forbid`, but the following do:\n\
579            {}\n",
580            forbid_groups.iter().map(|g| g.name).join("\n")
581        );
582    }
583
584    #[test]
585    fn ensure_sorted_lints() {
586        // This will be printed out if the fields are not sorted.
587        let location = std::panic::Location::caller();
588        println!("\nTo fix this test, sort `LINTS` in {}\n", location.file(),);
589
590        let actual = super::LINTS
591            .iter()
592            .map(|l| l.name.to_uppercase())
593            .collect::<Vec<_>>();
594
595        let mut expected = actual.clone();
596        expected.sort();
597        snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
598    }
599
600    #[test]
601    fn ensure_sorted_lint_groups() {
602        // This will be printed out if the fields are not sorted.
603        let location = std::panic::Location::caller();
604        println!(
605            "\nTo fix this test, sort `LINT_GROUPS` in {}\n",
606            location.file(),
607        );
608        let actual = super::LINT_GROUPS
609            .iter()
610            .map(|l| l.name.to_uppercase())
611            .collect::<Vec<_>>();
612
613        let mut expected = actual.clone();
614        expected.sort();
615        snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
616    }
617
618    #[test]
619    fn ensure_updated_lints() {
620        let dir = snapbox::utils::current_dir!().join("rules");
621        let mut expected = HashSet::new();
622        for entry in std::fs::read_dir(&dir).unwrap() {
623            let entry = entry.unwrap();
624            let path = entry.path();
625            if path.ends_with("mod.rs") {
626                continue;
627            }
628            let lint_name = path.file_stem().unwrap().to_string_lossy();
629            assert!(expected.insert(lint_name.into()), "duplicate lint found");
630        }
631
632        let actual = super::LINTS
633            .iter()
634            .map(|l| l.name.to_string())
635            .collect::<HashSet<_>>();
636        let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
637
638        let mut need_added = String::new();
639        for name in &diff {
640            need_added.push_str(&format!("{name}\n"));
641        }
642        assert!(
643            diff.is_empty(),
644            "\n`LINTS` did not contain all `Lint`s found in {}\n\
645            Please add the following to `LINTS`:\n\
646            {need_added}",
647            dir.display(),
648        );
649    }
650
651    #[test]
652    fn ensure_updated_lint_groups() {
653        let path = snapbox::utils::current_rs!();
654        let expected = std::fs::read_to_string(&path).unwrap();
655        let expected = expected
656            .lines()
657            .filter_map(|l| {
658                if l.ends_with(": LintGroup = LintGroup {") {
659                    Some(
660                        l.chars()
661                            .skip(6)
662                            .take_while(|c| *c != ':')
663                            .collect::<String>(),
664                    )
665                } else {
666                    None
667                }
668            })
669            .collect::<HashSet<_>>();
670        let actual = super::LINT_GROUPS
671            .iter()
672            .map(|l| l.name.to_uppercase())
673            .collect::<HashSet<_>>();
674        let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
675
676        let mut need_added = String::new();
677        for name in &diff {
678            need_added.push_str(&format!("{}\n", name));
679        }
680        assert!(
681            diff.is_empty(),
682            "\n`LINT_GROUPS` did not contain all `LintGroup`s found in {}\n\
683            Please add the following to `LINT_GROUPS`:\n\
684            {}",
685            path.display(),
686            need_added
687        );
688    }
689}