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(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 usize {
254    fn as_index<'i>(&'i self) -> TomlIndex<'i> {
255        TomlIndex::Offset(*self)
256    }
257}
258
259pub fn get_key_value<'doc, 'i>(
260    document: &'doc toml::Spanned<toml::de::DeTable<'static>>,
261    path: &[impl AsIndex],
262) -> Option<(
263    &'doc toml::Spanned<Cow<'doc, str>>,
264    &'doc toml::Spanned<toml::de::DeValue<'static>>,
265)> {
266    let table = document.get_ref();
267    let mut iter = path.into_iter();
268    let index0 = iter.next()?.as_index();
269    let key0 = index0.as_key()?;
270    let (mut current_key, mut current_item) = table.get_key_value(key0)?;
271
272    while let Some(index) = iter.next() {
273        match index.as_index() {
274            TomlIndex::Key(key) => {
275                if let Some(table) = current_item.get_ref().as_table() {
276                    (current_key, current_item) = table.get_key_value(key)?;
277                } else if let Some(array) = current_item.get_ref().as_array() {
278                    current_item = array.iter().find(|item| match item.get_ref() {
279                        toml::de::DeValue::String(s) => s == key,
280                        _ => false,
281                    })?;
282                } else {
283                    return None;
284                }
285            }
286            TomlIndex::Offset(offset) => {
287                let array = current_item.get_ref().as_array()?;
288                current_item = array.get(offset)?;
289            }
290        }
291    }
292    Some((current_key, current_item))
293}
294
295pub fn get_key_value_span<'i>(
296    document: &toml::Spanned<toml::de::DeTable<'static>>,
297    path: &[impl AsIndex],
298) -> Option<TomlSpan> {
299    get_key_value(document, path).map(|(k, v)| TomlSpan {
300        key: k.span(),
301        value: v.span(),
302    })
303}
304
305/// Gets the relative path to a manifest from the current working directory, or
306/// the absolute path of the manifest if a relative path cannot be constructed
307pub fn rel_cwd_manifest_path(path: &Path, gctx: &GlobalContext) -> String {
308    diff_paths(path, gctx.cwd())
309        .unwrap_or_else(|| path.to_path_buf())
310        .display()
311        .to_string()
312}
313
314#[derive(Clone, Debug)]
315pub struct LintGroup {
316    pub name: &'static str,
317    pub default_level: LintLevel,
318    pub desc: &'static str,
319    pub feature_gate: Option<&'static Feature>,
320    pub hidden: bool,
321}
322
323const COMPLEXITY: LintGroup = LintGroup {
324    name: "complexity",
325    desc: "code that does something simple but in a complex way",
326    default_level: LintLevel::Warn,
327    feature_gate: None,
328    hidden: false,
329};
330
331const CORRECTNESS: LintGroup = LintGroup {
332    name: "correctness",
333    desc: "code that is outright wrong or useless",
334    default_level: LintLevel::Deny,
335    feature_gate: None,
336    hidden: false,
337};
338
339const NURSERY: LintGroup = LintGroup {
340    name: "nursery",
341    desc: "new lints that are still under development",
342    default_level: LintLevel::Allow,
343    feature_gate: None,
344    hidden: false,
345};
346
347const PEDANTIC: LintGroup = LintGroup {
348    name: "pedantic",
349    desc: "lints which are rather strict or have occasional false positives",
350    default_level: LintLevel::Allow,
351    feature_gate: None,
352    hidden: false,
353};
354
355const PERF: LintGroup = LintGroup {
356    name: "perf",
357    desc: "code that can be written to run faster",
358    default_level: LintLevel::Warn,
359    feature_gate: None,
360    hidden: false,
361};
362
363const RESTRICTION: LintGroup = LintGroup {
364    name: "restriction",
365    desc: "lints which prevent the use of Cargo features",
366    default_level: LintLevel::Allow,
367    feature_gate: None,
368    hidden: false,
369};
370
371const STYLE: LintGroup = LintGroup {
372    name: "style",
373    desc: "code that should be written in a more idiomatic way",
374    default_level: LintLevel::Warn,
375    feature_gate: None,
376    hidden: false,
377};
378
379const SUSPICIOUS: LintGroup = LintGroup {
380    name: "suspicious",
381    desc: "code that is most likely wrong or useless",
382    default_level: LintLevel::Warn,
383    feature_gate: None,
384    hidden: false,
385};
386
387/// This lint group is only to be used for testing purposes
388const TEST_DUMMY_UNSTABLE: LintGroup = LintGroup {
389    name: "test_dummy_unstable",
390    desc: "test_dummy_unstable is meant to only be used in tests",
391    default_level: LintLevel::Allow,
392    feature_gate: Some(Feature::test_dummy_unstable()),
393    hidden: true,
394};
395
396#[derive(Clone, Debug)]
397pub struct Lint {
398    pub name: &'static str,
399    pub desc: &'static str,
400    pub primary_group: &'static LintGroup,
401    /// The minimum supported Rust version for applying this lint
402    ///
403    /// Note: If the lint is on by default and did not qualify as a hard-warning before the
404    /// linting system, then at earliest an MSRV of 1.78 is required as `[lints.cargo]` was a hard
405    /// error before then.
406    pub msrv: Option<RustVersion>,
407    pub feature_gate: Option<&'static Feature>,
408    /// This is a markdown formatted string that will be used when generating
409    /// the lint documentation. If docs is `None`, the lint will not be
410    /// documented.
411    pub docs: Option<&'static str>,
412}
413
414impl Lint {
415    pub fn level(
416        &self,
417        pkg_lints: &TomlToolLints,
418        pkg_rust_version: Option<&RustVersion>,
419        unstable_features: &Features,
420    ) -> (LintLevel, LintLevelReason) {
421        // We should return `Allow` if a lint is behind a feature, but it is
422        // not enabled, that way the lint does not run.
423        if self
424            .feature_gate
425            .is_some_and(|f| !unstable_features.is_enabled(f))
426        {
427            return (LintLevel::Allow, LintLevelReason::Default);
428        }
429
430        if let (Some(msrv), Some(pkg_rust_version)) = (&self.msrv, pkg_rust_version) {
431            let pkg_rust_version = pkg_rust_version.to_partial();
432            if !msrv.is_compatible_with(&pkg_rust_version) {
433                return (LintLevel::Allow, LintLevelReason::Default);
434            }
435        }
436
437        let lint_level_priority =
438            level_priority(self.name, self.primary_group.default_level, pkg_lints);
439
440        let group_level_priority = level_priority(
441            self.primary_group.name,
442            self.primary_group.default_level,
443            pkg_lints,
444        );
445
446        let (_, (l, r, _)) = max_by_key(
447            (self.name, lint_level_priority),
448            (self.primary_group.name, group_level_priority),
449            |(n, (l, _, p))| (l == &LintLevel::Forbid, *p, Reverse(*n)),
450        );
451        (l, r)
452    }
453
454    fn emitted_source(&self, lint_level: LintLevel, reason: LintLevelReason) -> String {
455        format!("`cargo::{}` is set to `{lint_level}` {reason}", self.name,)
456    }
457}
458
459#[derive(Copy, Clone, Debug, PartialEq)]
460pub enum LintLevel {
461    Allow,
462    Warn,
463    Deny,
464    Forbid,
465}
466
467impl Display for LintLevel {
468    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
469        match self {
470            LintLevel::Allow => write!(f, "allow"),
471            LintLevel::Warn => write!(f, "warn"),
472            LintLevel::Deny => write!(f, "deny"),
473            LintLevel::Forbid => write!(f, "forbid"),
474        }
475    }
476}
477
478impl LintLevel {
479    pub fn is_error(&self) -> bool {
480        self == &LintLevel::Forbid || self == &LintLevel::Deny
481    }
482
483    pub fn to_diagnostic_level(self) -> Level<'static> {
484        match self {
485            LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
486            LintLevel::Warn => Level::WARNING,
487            LintLevel::Deny => Level::ERROR,
488            LintLevel::Forbid => Level::ERROR,
489        }
490    }
491
492    fn force(self) -> bool {
493        match self {
494            Self::Allow => false,
495            Self::Warn => true,
496            Self::Deny => true,
497            Self::Forbid => true,
498        }
499    }
500}
501
502impl From<TomlLintLevel> for LintLevel {
503    fn from(toml_lint_level: TomlLintLevel) -> LintLevel {
504        match toml_lint_level {
505            TomlLintLevel::Allow => LintLevel::Allow,
506            TomlLintLevel::Warn => LintLevel::Warn,
507            TomlLintLevel::Deny => LintLevel::Deny,
508            TomlLintLevel::Forbid => LintLevel::Forbid,
509        }
510    }
511}
512
513#[derive(Copy, Clone, Debug, PartialEq, Eq)]
514pub enum LintLevelReason {
515    Default,
516    Package,
517}
518
519impl Display for LintLevelReason {
520    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521        match self {
522            LintLevelReason::Default => write!(f, "by default"),
523            LintLevelReason::Package => write!(f, "in `[lints]`"),
524        }
525    }
526}
527
528impl LintLevelReason {
529    fn is_user_specified(&self) -> bool {
530        match self {
531            LintLevelReason::Default => false,
532            LintLevelReason::Package => true,
533        }
534    }
535}
536
537fn level_priority(
538    name: &str,
539    default_level: LintLevel,
540    pkg_lints: &TomlToolLints,
541) -> (LintLevel, LintLevelReason, i8) {
542    if let Some(defined_level) = pkg_lints.get(name) {
543        (
544            defined_level.level().into(),
545            LintLevelReason::Package,
546            defined_level.priority(),
547        )
548    } else {
549        (default_level, LintLevelReason::Default, 0)
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use itertools::Itertools;
556    use snapbox::ToDebug;
557    use std::collections::HashSet;
558
559    #[test]
560    fn ensure_lint_groups_do_not_default_to_forbid() {
561        let forbid_groups = super::LINT_GROUPS
562            .iter()
563            .filter(|g| matches!(g.default_level, super::LintLevel::Forbid))
564            .collect::<Vec<_>>();
565
566        assert!(
567            forbid_groups.is_empty(),
568            "\n`LintGroup`s should never default to `forbid`, but the following do:\n\
569            {}\n",
570            forbid_groups.iter().map(|g| g.name).join("\n")
571        );
572    }
573
574    #[test]
575    fn ensure_sorted_lints() {
576        // This will be printed out if the fields are not sorted.
577        let location = std::panic::Location::caller();
578        println!("\nTo fix this test, sort `LINTS` in {}\n", location.file(),);
579
580        let actual = super::LINTS
581            .iter()
582            .map(|l| l.name.to_uppercase())
583            .collect::<Vec<_>>();
584
585        let mut expected = actual.clone();
586        expected.sort();
587        snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
588    }
589
590    #[test]
591    fn ensure_sorted_lint_groups() {
592        // This will be printed out if the fields are not sorted.
593        let location = std::panic::Location::caller();
594        println!(
595            "\nTo fix this test, sort `LINT_GROUPS` in {}\n",
596            location.file(),
597        );
598        let actual = super::LINT_GROUPS
599            .iter()
600            .map(|l| l.name.to_uppercase())
601            .collect::<Vec<_>>();
602
603        let mut expected = actual.clone();
604        expected.sort();
605        snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
606    }
607
608    #[test]
609    fn ensure_updated_lints() {
610        let dir = snapbox::utils::current_dir!().join("rules");
611        let mut expected = HashSet::new();
612        for entry in std::fs::read_dir(&dir).unwrap() {
613            let entry = entry.unwrap();
614            let path = entry.path();
615            if path.ends_with("mod.rs") {
616                continue;
617            }
618            let lint_name = path.file_stem().unwrap().to_string_lossy();
619            assert!(expected.insert(lint_name.into()), "duplicate lint found");
620        }
621
622        let actual = super::LINTS
623            .iter()
624            .map(|l| l.name.to_string())
625            .collect::<HashSet<_>>();
626        let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
627
628        let mut need_added = String::new();
629        for name in &diff {
630            need_added.push_str(&format!("{name}\n"));
631        }
632        assert!(
633            diff.is_empty(),
634            "\n`LINTS` did not contain all `Lint`s found in {}\n\
635            Please add the following to `LINTS`:\n\
636            {need_added}",
637            dir.display(),
638        );
639    }
640
641    #[test]
642    fn ensure_updated_lint_groups() {
643        let path = snapbox::utils::current_rs!();
644        let expected = std::fs::read_to_string(&path).unwrap();
645        let expected = expected
646            .lines()
647            .filter_map(|l| {
648                if l.ends_with(": LintGroup = LintGroup {") {
649                    Some(
650                        l.chars()
651                            .skip(6)
652                            .take_while(|c| *c != ':')
653                            .collect::<String>(),
654                    )
655                } else {
656                    None
657                }
658            })
659            .collect::<HashSet<_>>();
660        let actual = super::LINT_GROUPS
661            .iter()
662            .map(|l| l.name.to_uppercase())
663            .collect::<HashSet<_>>();
664        let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
665
666        let mut need_added = String::new();
667        for name in &diff {
668            need_added.push_str(&format!("{}\n", name));
669        }
670        assert!(
671            diff.is_empty(),
672            "\n`LINT_GROUPS` did not contain all `LintGroup`s found in {}\n\
673            Please add the following to `LINT_GROUPS`:\n\
674            {}",
675            path.display(),
676            need_added
677        );
678    }
679}