Skip to main content

cargo/diagnostics/rules/
unknown_lints.rs

1use std::path::Path;
2
3use cargo_util_schemas::manifest::TomlToolLints;
4use cargo_util_terminal::report::AnnotationKind;
5use cargo_util_terminal::report::Group;
6use cargo_util_terminal::report::Level;
7use cargo_util_terminal::report::Origin;
8use cargo_util_terminal::report::Snippet;
9use tracing::instrument;
10
11use super::LINT_GROUPS;
12use super::LINTS;
13use super::SUSPICIOUS;
14use super::find_lint_or_group;
15use crate::CargoResult;
16use crate::GlobalContext;
17use crate::core::MaybePackage;
18use crate::diagnostics::DiagnosticStats;
19use crate::diagnostics::Lint;
20use crate::diagnostics::LintLevelProduct;
21use crate::diagnostics::ManifestFor;
22use crate::diagnostics::get_key_value_span;
23use crate::diagnostics::rel_cwd_manifest_path;
24
25pub static LINT: &Lint = &Lint {
26    name: "unknown_lints",
27    desc: "unknown lint",
28    primary_group: &SUSPICIOUS,
29    msrv: Some(super::CARGO_LINTS_MSRV),
30    feature_gate: None,
31    docs: Some(
32        r#"
33### What it does
34Checks for unknown lints in the `[lints.cargo]` table
35
36### Why it is bad
37- The lint name could be misspelled, leading to confusion as to why it is
38  not working as expected
39- The unknown lint could end up causing an error if `cargo` decides to make
40  a lint with the same name in the future
41
42### Example
43```toml
44[lints.cargo]
45this-lint-does-not-exist = "warn"
46```
47"#,
48    ),
49};
50
51#[instrument(skip_all)]
52pub(crate) fn lint_manifest(
53    manifest: ManifestFor<'_>,
54    manifest_path: &Path,
55    level: LintLevelProduct,
56    pkg_stats: &mut DiagnosticStats,
57    gctx: &GlobalContext,
58) -> CargoResult<()> {
59    let normalized_toml = match &manifest {
60        ManifestFor::Package(pkg) => pkg.manifest().normalized_toml(),
61        ManifestFor::Workspace {
62            maybe_pkg: MaybePackage::Virtual(vm),
63            ..
64        } => vm.normalized_toml(),
65        ManifestFor::Workspace {
66            maybe_pkg: MaybePackage::Package(_),
67            ..
68        } => {
69            // For real manifests, lint as a package, rather than a workspace
70            return Ok(());
71        }
72    };
73
74    let ws_lints = normalized_toml
75        .workspace
76        .as_ref()
77        .and_then(|ws| ws.lints.as_ref())
78        .and_then(|lints| lints.get("cargo"));
79    let pkg_lints = normalized_toml
80        .lints
81        .as_ref()
82        .map(|lints| &lints.lints)
83        .and_then(|lints| lints.get("cargo"));
84
85    if let Some(cargo_lints) = ws_lints {
86        lint_manifest_inner(
87            &manifest,
88            manifest_path,
89            &level,
90            cargo_lints,
91            pkg_stats,
92            gctx,
93        )?;
94    }
95    if let Some(cargo_lints) = pkg_lints {
96        lint_manifest_inner(
97            &manifest,
98            manifest_path,
99            &level,
100            cargo_lints,
101            pkg_stats,
102            gctx,
103        )?;
104    }
105
106    Ok(())
107}
108
109fn lint_manifest_inner(
110    manifest: &ManifestFor<'_>,
111    manifest_path: &Path,
112    level: &LintLevelProduct,
113    cargo_lints: &TomlToolLints,
114    pkg_stats: &mut DiagnosticStats,
115    gctx: &GlobalContext,
116) -> CargoResult<()> {
117    let LintLevelProduct {
118        level: lint_level,
119        source,
120    } = level;
121
122    let manifest_path = rel_cwd_manifest_path(manifest_path, gctx);
123    let mut unknown_lints = Vec::new();
124    for lint_name in cargo_lints.keys().map(|name| name) {
125        let Some(_) = find_lint_or_group(lint_name) else {
126            unknown_lints.push(lint_name);
127            continue;
128        };
129    }
130
131    let level = lint_level.to_diagnostic_level();
132    let mut emitted_source = None;
133    for lint_name in unknown_lints {
134        let title = format!("{}: `{lint_name}`", LINT.desc);
135        let underscore_lint_name = lint_name.replace("-", "_");
136        let matching = if let Some(lint) = LINTS.iter().find(|l| l.name == underscore_lint_name) {
137            Some((lint.name, "lint"))
138        } else if let Some(group) = LINT_GROUPS.iter().find(|g| g.name == underscore_lint_name) {
139            Some((group.name, "group"))
140        } else {
141            None
142        };
143        let help =
144            matching.map(|(name, kind)| format!("there is a {kind} with a similar name: `{name}`"));
145
146        let key_path = match manifest {
147            ManifestFor::Package(_) => &["lints", "cargo", lint_name][..],
148            ManifestFor::Workspace { .. } => &["workspace", "lints", "cargo", lint_name][..],
149        };
150
151        let mut report = Vec::new();
152        let mut group = Group::with_title(level.clone().primary_title(title));
153
154        if let Some(document) = manifest.document()
155            && let Some(contents) = manifest.contents()
156        {
157            let Some(span) = get_key_value_span(document, key_path) else {
158                // This lint is handled by either package or workspace lint.
159                return Ok(());
160            };
161            group = group.element(
162                Snippet::source(contents)
163                    .path(&manifest_path)
164                    .annotation(AnnotationKind::Primary.span(span.key)),
165            );
166        } else {
167            group = group.element(Origin::path(&manifest_path));
168        }
169
170        if emitted_source.is_none() {
171            emitted_source = Some(LINT.emitted_source(*lint_level, *source));
172            group = group.element(Level::NOTE.message(emitted_source.as_ref().unwrap()));
173        }
174        if let Some(help) = help.as_ref() {
175            group = group.element(Level::HELP.message(help));
176        }
177        report.push(group);
178
179        pkg_stats.record_lint(*lint_level);
180        gctx.shell().print_report(&report, lint_level.force())?;
181    }
182
183    Ok(())
184}