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, LintLevelSource) {
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 (_, source, _) = level_priority(name, *default_level, cargo_lints);
115
116        // Only run analysis on user-specified lints
117        if !source.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, LintLevelSource) {
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, LintLevelSource::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, LintLevelSource::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, s, _)) = max_by_key(
453            (self.name, lint_level_priority),
454            (self.primary_group.name, group_level_priority),
455            |(n, (l, s, p))| {
456                (
457                    l == &LintLevel::Forbid,
458                    *s != LintLevelSource::Default,
459                    *p,
460                    Reverse(*n),
461                )
462            },
463        );
464        (l, s)
465    }
466
467    pub fn emitted_source(&self, lint_level: LintLevel, source: LintLevelSource) -> String {
468        format!("`cargo::{}` is set to `{lint_level}` {source}", self.name,)
469    }
470}
471
472#[derive(Copy, Clone, Debug, PartialEq)]
473pub enum LintLevel {
474    Allow,
475    Warn,
476    Deny,
477    Forbid,
478}
479
480impl Display for LintLevel {
481    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482        match self {
483            LintLevel::Allow => write!(f, "allow"),
484            LintLevel::Warn => write!(f, "warn"),
485            LintLevel::Deny => write!(f, "deny"),
486            LintLevel::Forbid => write!(f, "forbid"),
487        }
488    }
489}
490
491impl LintLevel {
492    pub fn is_warn(&self) -> bool {
493        self == &LintLevel::Warn
494    }
495
496    pub fn is_error(&self) -> bool {
497        self == &LintLevel::Forbid || self == &LintLevel::Deny
498    }
499
500    pub fn to_diagnostic_level(self) -> Level<'static> {
501        match self {
502            LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
503            LintLevel::Warn => Level::WARNING,
504            LintLevel::Deny => Level::ERROR,
505            LintLevel::Forbid => Level::ERROR,
506        }
507    }
508
509    pub fn force(self) -> bool {
510        match self {
511            Self::Allow => false,
512            Self::Warn => true,
513            Self::Deny => true,
514            Self::Forbid => true,
515        }
516    }
517}
518
519impl From<TomlLintLevel> for LintLevel {
520    fn from(toml_lint_level: TomlLintLevel) -> LintLevel {
521        match toml_lint_level {
522            TomlLintLevel::Allow => LintLevel::Allow,
523            TomlLintLevel::Warn => LintLevel::Warn,
524            TomlLintLevel::Deny => LintLevel::Deny,
525            TomlLintLevel::Forbid => LintLevel::Forbid,
526        }
527    }
528}
529
530#[derive(Copy, Clone, Debug, PartialEq, Eq)]
531pub enum LintLevelSource {
532    Default,
533    Package,
534}
535
536impl Display for LintLevelSource {
537    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
538        match self {
539            LintLevelSource::Default => write!(f, "by default"),
540            LintLevelSource::Package => write!(f, "in `[lints]`"),
541        }
542    }
543}
544
545impl LintLevelSource {
546    fn is_user_specified(&self) -> bool {
547        match self {
548            LintLevelSource::Default => false,
549            LintLevelSource::Package => true,
550        }
551    }
552}
553
554fn level_priority(
555    name: &str,
556    default_level: LintLevel,
557    pkg_lints: &TomlToolLints,
558) -> (LintLevel, LintLevelSource, i8) {
559    if let Some(defined_level) = pkg_lints.get(name) {
560        (
561            defined_level.level().into(),
562            LintLevelSource::Package,
563            defined_level.priority(),
564        )
565    } else {
566        (default_level, LintLevelSource::Default, 0)
567    }
568}
569
570#[cfg(test)]
571mod tests {
572    use itertools::Itertools;
573    use snapbox::ToDebug;
574    use std::collections::HashSet;
575
576    use super::*;
577
578    fn test_lint(name: &'static str, group: &'static LintGroup) -> Lint {
579        Lint {
580            name,
581            desc: "test lint",
582            primary_group: group,
583            msrv: None,
584            feature_gate: None,
585            docs: None,
586        }
587    }
588
589    #[test]
590    fn lint_level_prefers_user_specified_over_default() {
591        let lint = test_lint("unused_dependencies", &STYLE);
592
593        let mut pkg_lints = TomlToolLints::new();
594        pkg_lints.insert(
595            "unused_dependencies".to_string(),
596            cargo_util_schemas::manifest::TomlLint::Level(TomlLintLevel::Deny),
597        );
598        let features = Features::default();
599
600        let (level, source) = lint.level(&pkg_lints, None, &features);
601        assert_eq!(level, LintLevel::Deny);
602        assert_eq!(source, LintLevelSource::Package);
603    }
604
605    #[test]
606    fn lint_level_group_overrides_default() {
607        let lint = test_lint("non_kebab_case_bins", &STYLE);
608
609        let mut pkg_lints = TomlToolLints::new();
610        pkg_lints.insert(
611            "style".to_string(),
612            cargo_util_schemas::manifest::TomlLint::Level(TomlLintLevel::Deny),
613        );
614        let features = Features::default();
615
616        let (level, source) = lint.level(&pkg_lints, None, &features);
617        assert_eq!(level, LintLevel::Deny);
618        assert_eq!(source, LintLevelSource::Package);
619    }
620
621    #[test]
622    fn ensure_lint_groups_do_not_default_to_forbid() {
623        let forbid_groups = super::LINT_GROUPS
624            .iter()
625            .filter(|g| matches!(g.default_level, super::LintLevel::Forbid))
626            .collect::<Vec<_>>();
627
628        assert!(
629            forbid_groups.is_empty(),
630            "\n`LintGroup`s should never default to `forbid`, but the following do:\n\
631            {}\n",
632            forbid_groups.iter().map(|g| g.name).join("\n")
633        );
634    }
635
636    #[test]
637    fn ensure_sorted_lints() {
638        // This will be printed out if the fields are not sorted.
639        let location = std::panic::Location::caller();
640        println!("\nTo fix this test, sort `LINTS` in {}\n", location.file(),);
641
642        let actual = super::LINTS
643            .iter()
644            .map(|l| l.name.to_uppercase())
645            .collect::<Vec<_>>();
646
647        let mut expected = actual.clone();
648        expected.sort();
649        snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
650    }
651
652    #[test]
653    fn ensure_sorted_lint_groups() {
654        // This will be printed out if the fields are not sorted.
655        let location = std::panic::Location::caller();
656        println!(
657            "\nTo fix this test, sort `LINT_GROUPS` in {}\n",
658            location.file(),
659        );
660        let actual = super::LINT_GROUPS
661            .iter()
662            .map(|l| l.name.to_uppercase())
663            .collect::<Vec<_>>();
664
665        let mut expected = actual.clone();
666        expected.sort();
667        snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
668    }
669
670    #[test]
671    fn ensure_updated_lints() {
672        let dir = snapbox::utils::current_dir!().join("rules");
673        let mut expected = HashSet::new();
674        for entry in std::fs::read_dir(&dir).unwrap() {
675            let entry = entry.unwrap();
676            let path = entry.path();
677            if path.ends_with("mod.rs") {
678                continue;
679            }
680            let lint_name = path.file_stem().unwrap().to_string_lossy();
681            assert!(expected.insert(lint_name.into()), "duplicate lint found");
682        }
683
684        let actual = super::LINTS
685            .iter()
686            .map(|l| l.name.to_string())
687            .collect::<HashSet<_>>();
688        let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
689
690        let mut need_added = String::new();
691        for name in &diff {
692            need_added.push_str(&format!("{name}\n"));
693        }
694        assert!(
695            diff.is_empty(),
696            "\n`LINTS` did not contain all `Lint`s found in {}\n\
697            Please add the following to `LINTS`:\n\
698            {need_added}",
699            dir.display(),
700        );
701    }
702
703    #[test]
704    fn ensure_updated_lint_groups() {
705        let path = snapbox::utils::current_rs!();
706        let expected = std::fs::read_to_string(&path).unwrap();
707        let expected = expected
708            .lines()
709            .filter_map(|l| {
710                if l.ends_with(": LintGroup = LintGroup {") {
711                    Some(
712                        l.chars()
713                            .skip(6)
714                            .take_while(|c| *c != ':')
715                            .collect::<String>(),
716                    )
717                } else {
718                    None
719                }
720            })
721            .collect::<HashSet<_>>();
722        let actual = super::LINT_GROUPS
723            .iter()
724            .map(|l| l.name.to_uppercase())
725            .collect::<HashSet<_>>();
726        let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
727
728        let mut need_added = String::new();
729        for name in &diff {
730            need_added.push_str(&format!("{}\n", name));
731        }
732        assert!(
733            diff.is_empty(),
734            "\n`LINT_GROUPS` did not contain all `LintGroup`s found in {}\n\
735            Please add the following to `LINT_GROUPS`:\n\
736            {}",
737            path.display(),
738            need_added
739        );
740    }
741}