Skip to main content

cargo/lints/
mod.rs

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