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