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