diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index bc49b74d687..e7bbb6cab88 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -1161,7 +1161,6 @@ impl<'gctx> Workspace<'gctx> { .cloned() .unwrap_or_default() .into_iter() - .map(|(k, v)| (k.replace('-', "_"), v)) .collect(); for (path, maybe_pkg) in &self.packages.packages { @@ -1214,10 +1213,6 @@ impl<'gctx> Workspace<'gctx> { .get("cargo") .cloned() .unwrap_or(manifest::TomlToolLints::default()); - let normalized_lints = cargo_lints - .into_iter() - .map(|(name, lint)| (name.replace('-', "_"), lint)) - .collect(); // We should only be using workspace lints if the `[lints]` table is // present in the manifest, and `workspace` is set to `true` @@ -1242,7 +1237,7 @@ impl<'gctx> Workspace<'gctx> { analyze_cargo_lints_table( pkg, &path, - &normalized_lints, + &cargo_lints, ws_cargo_lints, ws_contents, ws_document, @@ -1252,7 +1247,7 @@ impl<'gctx> Workspace<'gctx> { check_im_a_teapot( pkg, &path, - &normalized_lints, + &cargo_lints, ws_cargo_lints, &mut error_count, self.gctx, @@ -1260,7 +1255,7 @@ impl<'gctx> Workspace<'gctx> { check_implicit_features( pkg, &path, - &normalized_lints, + &cargo_lints, ws_cargo_lints, &mut error_count, self.gctx, @@ -1268,7 +1263,7 @@ impl<'gctx> Workspace<'gctx> { unused_dependencies( pkg, &path, - &normalized_lints, + &cargo_lints, ws_cargo_lints, &mut error_count, self.gctx, diff --git a/src/cargo/util/lints.rs b/src/cargo/util/lints.rs index 5f09249342a..1d47872f440 100644 --- a/src/cargo/util/lints.rs +++ b/src/cargo/util/lints.rs @@ -29,45 +29,68 @@ pub fn analyze_cargo_lints_table( let manifest = pkg.manifest(); let manifest_path = rel_cwd_manifest_path(path, gctx); let ws_path = rel_cwd_manifest_path(ws_path, gctx); - - for lint_name in pkg_lints + let mut unknown_lints = Vec::new(); + for (lint_name, specified_in) in pkg_lints .keys() - .chain(ws_lints.map(|l| l.keys()).unwrap_or_default()) + .map(|name| (name, SpecifiedIn::Package)) + .chain( + ws_lints + .map(|l| l.keys()) + .unwrap_or_default() + .map(|name| (name, SpecifiedIn::Workspace)), + ) { - if let Some((name, default_level, edition_lint_opts, feature_gate)) = + let Some((name, default_level, edition_lint_opts, feature_gate)) = find_lint_or_group(lint_name) - { - let (_, reason, _) = level_priority( - name, - *default_level, - *edition_lint_opts, - pkg_lints, - ws_lints, - manifest.edition(), - ); - - // Only run analysis on user-specified lints - if !reason.is_user_specified() { - continue; - } + else { + unknown_lints.push((lint_name, specified_in)); + continue; + }; - // Only run this on lints that are gated by a feature - if let Some(feature_gate) = feature_gate { - verify_feature_enabled( - name, - feature_gate, - reason, - manifest, - &manifest_path, - ws_contents, - ws_document, - &ws_path, - &mut error_count, - gctx, - )?; - } + let (_, reason, _) = level_priority( + name, + *default_level, + *edition_lint_opts, + pkg_lints, + ws_lints, + manifest.edition(), + ); + + // Only run analysis on user-specified lints + if !reason.is_user_specified() { + continue; + } + + // Only run this on lints that are gated by a feature + if let Some(feature_gate) = feature_gate { + verify_feature_enabled( + name, + feature_gate, + reason, + manifest, + &manifest_path, + ws_contents, + ws_document, + &ws_path, + &mut error_count, + gctx, + )?; } } + + output_unknown_lints( + unknown_lints, + manifest, + &manifest_path, + pkg_lints, + ws_lints, + ws_contents, + ws_document, + &ws_path, + &mut error_count, + gctx, + )?; + if error_count > 0 { Err(anyhow::anyhow!( "encountered {error_count} errors(s) while verifying lints", @@ -117,31 +140,21 @@ fn verify_feature_enabled( gctx: &GlobalContext, ) -> CargoResult<()> { if !manifest.unstable_features().is_enabled(feature_gate) { - let dash_name = lint_name.replace("_", "-"); let dash_feature_name = feature_gate.name().replace("_", "-"); - let title = format!("use of unstable lint `{}`", dash_name); + let title = format!("use of unstable lint `{}`", lint_name); let label = format!( "this is behind `{}`, which is not enabled", dash_feature_name ); - let second_title = format!("`cargo::{}` was inherited", dash_name); + let second_title = format!("`cargo::{}` was inherited", lint_name); let help = format!( "consider adding `cargo-features = [\"{}\"]` to the top of the manifest", dash_feature_name ); let message = match reason { LintLevelReason::Package => { - let span = get_span( - manifest.document(), - &["lints", "cargo", dash_name.as_str()], - false, - ) - .or(get_span( - manifest.document(), - &["lints", "cargo", lint_name], - false, - )) - .unwrap(); + let span = + get_span(manifest.document(), &["lints", "cargo", lint_name], false).unwrap(); Level::Error .title(&title) @@ -155,15 +168,10 @@ fn verify_feature_enabled( } LintLevelReason::Workspace => { let lint_span = get_span( - ws_document, - &["workspace", "lints", "cargo", dash_name.as_str()], - false, - ) - .or(get_span( ws_document, &["workspace", "lints", "cargo", lint_name], false, - )) + ) .unwrap(); let inherit_span_key = get_span(manifest.document(), &["lints", "workspace"], false).unwrap(); @@ -395,6 +403,11 @@ impl LintLevelReason { } } +enum SpecifiedIn { + Package, + Workspace, +} + fn level_priority( name: &str, default_level: LintLevel, @@ -588,6 +601,120 @@ pub fn check_implicit_features( Ok(()) } +const UNKNOWN_LINTS: Lint = Lint { + name: "unknown_lints", + desc: "unknown lint", + groups: &[], + default_level: LintLevel::Warn, + edition_lint_opts: None, + feature_gate: None, +}; + +fn output_unknown_lints( + unknown_lints: Vec<(&String, SpecifiedIn)>, + manifest: &Manifest, + manifest_path: &str, + pkg_lints: &TomlToolLints, + ws_lints: Option<&TomlToolLints>, + ws_contents: &str, + ws_document: &ImDocument, + ws_path: &str, + error_count: &mut usize, + gctx: &GlobalContext, +) -> CargoResult<()> { + let (lint_level, reason) = UNKNOWN_LINTS.level( + pkg_lints, + ws_lints, + manifest.edition(), + manifest.unstable_features(), + ); + if lint_level == LintLevel::Allow { + return Ok(()); + } + + let level = lint_level.to_diagnostic_level(); + let mut emitted_source = None; + for (lint_name, specified_in) in unknown_lints { + if lint_level == LintLevel::Forbid || lint_level == LintLevel::Deny { + *error_count += 1; + } + let title = format!("{}: `{lint_name}`", UNKNOWN_LINTS.desc); + let second_title = format!("`cargo::{}` was inherited", lint_name); + let underscore_lint_name = lint_name.replace("-", "_"); + let matching = if let Some(lint) = LINTS.iter().find(|l| l.name == underscore_lint_name) { + Some((lint.name, "lint")) + } else if let Some(group) = LINT_GROUPS.iter().find(|g| g.name == underscore_lint_name) { + Some((group.name, "group")) + } else { + None + }; + let help = + matching.map(|(name, kind)| format!("there is a {kind} with a similar name: `{name}`")); + + let mut message = match specified_in { + SpecifiedIn::Package => { + let span = + get_span(manifest.document(), &["lints", "cargo", lint_name], false).unwrap(); + + level.title(&title).snippet( + Snippet::source(manifest.contents()) + .origin(&manifest_path) + .annotation(Level::Error.span(span)) + .fold(true), + ) + } + SpecifiedIn::Workspace => { + let lint_span = get_span( + ws_document, + &["workspace", "lints", "cargo", lint_name], + false, + ) + .unwrap(); + let inherit_span_key = + get_span(manifest.document(), &["lints", "workspace"], false).unwrap(); + let inherit_span_value = + get_span(manifest.document(), &["lints", "workspace"], true).unwrap(); + + level + .title(&title) + .snippet( + Snippet::source(ws_contents) + .origin(&ws_path) + .annotation(Level::Error.span(lint_span)) + .fold(true), + ) + .footer( + Level::Note.title(&second_title).snippet( + Snippet::source(manifest.contents()) + .origin(&manifest_path) + .annotation( + Level::Note + .span(inherit_span_key.start..inherit_span_value.end), + ) + .fold(true), + ), + ) + } + }; + + if emitted_source.is_none() { + emitted_source = Some(format!( + "`cargo::{}` is set to `{lint_level}` {reason}", + UNKNOWN_LINTS.name + )); + message = message.footer(Level::Note.title(emitted_source.as_ref().unwrap())); + } + + if let Some(help) = help.as_ref() { + message = message.footer(Level::Help.title(help)); + } + + gctx.shell().print_message(message)?; + } + + Ok(()) +} + const UNUSED_OPTIONAL_DEPENDENCY: Lint = Lint { name: "unused_optional_dependency", desc: "unused optional dependency", diff --git a/tests/testsuite/lints/implicit_features/edition_2021_warn/mod.rs b/tests/testsuite/lints/implicit_features/edition_2021_warn/mod.rs index a7169ea4d95..45f29b029fa 100644 --- a/tests/testsuite/lints/implicit_features/edition_2021_warn/mod.rs +++ b/tests/testsuite/lints/implicit_features/edition_2021_warn/mod.rs @@ -27,7 +27,7 @@ baz = { version = "0.1.0", optional = true } target-dep = { version = "0.1.0", optional = true } [lints.cargo] -implicit-features = "warn" +implicit_features = "warn" "#, ) .file("src/lib.rs", "") diff --git a/tests/testsuite/lints/implicit_features/edition_2024/mod.rs b/tests/testsuite/lints/implicit_features/edition_2024/mod.rs index 1913bc600ad..ca682aa8a5a 100644 --- a/tests/testsuite/lints/implicit_features/edition_2024/mod.rs +++ b/tests/testsuite/lints/implicit_features/edition_2024/mod.rs @@ -25,7 +25,7 @@ baz = { version = "0.1.0", optional = true } baz = ["dep:baz"] [lints.cargo] -unused-optional-dependency = "allow" +unused_optional_dependency = "allow" "#, ) .file("src/lib.rs", "") diff --git a/tests/testsuite/lints/mod.rs b/tests/testsuite/lints/mod.rs index 837198ce3b8..1daa7b7a340 100644 --- a/tests/testsuite/lints/mod.rs +++ b/tests/testsuite/lints/mod.rs @@ -1,2 +1,3 @@ mod implicit_features; +mod unknown_lints; mod unused_optional_dependencies; diff --git a/tests/testsuite/lints/unknown_lints/default/mod.rs b/tests/testsuite/lints/unknown_lints/default/mod.rs new file mode 100644 index 00000000000..d17596255ac --- /dev/null +++ b/tests/testsuite/lints/unknown_lints/default/mod.rs @@ -0,0 +1,33 @@ +use cargo_test_support::prelude::*; +use cargo_test_support::str; +use cargo_test_support::{file, project}; + +#[cargo_test] +fn case() { + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +version = "0.0.1" +edition = "2015" +authors = [] + +[lints.cargo] +this-lint-does-not-exist = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + snapbox::cmd::Command::cargo_ui() + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .current_dir(p.root()) + .arg("check") + .arg("-Zcargo-lints") + .assert() + .success() + .stdout_matches(str![""]) + .stderr_matches(file!["stderr.term.svg"]); +} diff --git a/tests/testsuite/lints/unknown_lints/default/stderr.term.svg b/tests/testsuite/lints/unknown_lints/default/stderr.term.svg new file mode 100644 index 00000000000..e3c89ca2cae --- /dev/null +++ b/tests/testsuite/lints/unknown_lints/default/stderr.term.svg @@ -0,0 +1,47 @@ + + + + + + + warning: unknown lint: `this-lint-does-not-exist` + + --> Cargo.toml:9:1 + + | + + 9 | this-lint-does-not-exist = "warn" + + | ^^^^^^^^^^^^^^^^^^^^^^^^ + + | + + = note: `cargo::unknown_lints` is set to `warn` by default + + Checking foo v0.0.1 ([ROOT]/foo) + + Finished [..] + + + + + + diff --git a/tests/testsuite/lints/unknown_lints/inherited/mod.rs b/tests/testsuite/lints/unknown_lints/inherited/mod.rs new file mode 100644 index 00000000000..4138ab4f5f3 --- /dev/null +++ b/tests/testsuite/lints/unknown_lints/inherited/mod.rs @@ -0,0 +1,43 @@ +use cargo_test_support::prelude::*; +use cargo_test_support::str; +use cargo_test_support::{file, project}; + +#[cargo_test] +fn case() { + let p = project() + .file( + "Cargo.toml", + r#" +[workspace] +members = ["foo"] + +[workspace.lints.cargo] +this-lint-does-not-exist = "warn" +"#, + ) + .file( + "foo/Cargo.toml", + r#" +[package] +name = "foo" +version = "0.0.1" +edition = "2015" +authors = [] + +[lints] +workspace = true + "#, + ) + .file("foo/src/lib.rs", "") + .build(); + + snapbox::cmd::Command::cargo_ui() + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .current_dir(p.root()) + .arg("check") + .arg("-Zcargo-lints") + .assert() + .success() + .stdout_matches(str![""]) + .stderr_matches(file!["stderr.term.svg"]); +} diff --git a/tests/testsuite/lints/unknown_lints/inherited/stderr.term.svg b/tests/testsuite/lints/unknown_lints/inherited/stderr.term.svg new file mode 100644 index 00000000000..aef9e175faf --- /dev/null +++ b/tests/testsuite/lints/unknown_lints/inherited/stderr.term.svg @@ -0,0 +1,59 @@ + + + + + + + warning: unknown lint: `this-lint-does-not-exist` + + --> Cargo.toml:6:1 + + | + + 6 | this-lint-does-not-exist = "warn" + + | ^^^^^^^^^^^^^^^^^^^^^^^^ + + | + + note: `cargo::this-lint-does-not-exist` was inherited + + --> foo/Cargo.toml:9:1 + + | + + 9 | workspace = true + + | ---------------- + + | + + = note: `cargo::unknown_lints` is set to `warn` by default + + Checking foo v0.0.1 ([ROOT]/foo/foo) + + Finished [..] + + + + + + diff --git a/tests/testsuite/lints/unknown_lints/mod.rs b/tests/testsuite/lints/unknown_lints/mod.rs new file mode 100644 index 00000000000..98324e98115 --- /dev/null +++ b/tests/testsuite/lints/unknown_lints/mod.rs @@ -0,0 +1,2 @@ +mod default; +mod inherited; diff --git a/tests/testsuite/lints/unused_optional_dependencies/edition_2021/mod.rs b/tests/testsuite/lints/unused_optional_dependencies/edition_2021/mod.rs index bf357f2739c..9a454b19131 100644 --- a/tests/testsuite/lints/unused_optional_dependencies/edition_2021/mod.rs +++ b/tests/testsuite/lints/unused_optional_dependencies/edition_2021/mod.rs @@ -19,7 +19,7 @@ edition = "2021" bar = { version = "0.1.0", optional = true } [lints.cargo] -implicit-features = "allow" +implicit_features = "allow" "#, ) .file("src/lib.rs", "") diff --git a/tests/testsuite/lints_table.rs b/tests/testsuite/lints_table.rs index 6434cd82d47..2a14da99088 100644 --- a/tests/testsuite/lints_table.rs +++ b/tests/testsuite/lints_table.rs @@ -762,7 +762,6 @@ edition = "2015" authors = [] [lints.cargo] -im-a-teapot = "warn" "#, ) .file("src/lib.rs", "") @@ -799,7 +798,6 @@ authors = [] im-a-teapot = true [lints.cargo] -im-a-teapot = "warn" "#, ) .file("src/lib.rs", "") @@ -835,7 +833,7 @@ authors = [] im-a-teapot = true [lints.cargo] -im-a-teapot = "warn" +im_a_teapot = "warn" "#, ) .file("src/lib.rs", "") @@ -860,7 +858,7 @@ warning: `im_a_teapot` is specified } #[cargo_test] -fn cargo_lints_underscore_supported() { +fn cargo_lints_dashes_dont_get_rewritten() { let foo = project() .file( "Cargo.toml", @@ -875,7 +873,7 @@ authors = [] im-a-teapot = true [lints.cargo] -im_a_teapot = "warn" +im-a-teapot = "warn" "#, ) .file("src/lib.rs", "") @@ -885,13 +883,14 @@ im_a_teapot = "warn" .masquerade_as_nightly_cargo(&["cargo-lints", "test-dummy-unstable"]) .with_stderr( "\ -warning: `im_a_teapot` is specified - --> Cargo.toml:9:1 - | -9 | im-a-teapot = true - | ------------------ - | - = note: `cargo::im_a_teapot` is set to `warn` in `[lints]` +warning: unknown lint: `im-a-teapot` + --> Cargo.toml:12:1 + | +12 | im-a-teapot = \"warn\" + | ^^^^^^^^^^^ + | + = note: `cargo::unknown_lints` is set to `warn` by default + = help: there is a lint with a similar name: `im_a_teapot` [CHECKING] foo v0.0.1 ([CWD]) [FINISHED] [..] ", @@ -915,8 +914,8 @@ authors = [] im-a-teapot = true [lints.cargo] -im-a-teapot = { level = "warn", priority = 10 } -test-dummy-unstable = { level = "forbid", priority = -1 } +im_a_teapot = { level = "warn", priority = 10 } +test_dummy_unstable = { level = "forbid", priority = -1 } "#, ) .file("src/lib.rs", "") @@ -948,8 +947,8 @@ fn workspace_cargo_lints() { cargo-features = ["test-dummy-unstable"] [workspace.lints.cargo] -im-a-teapot = { level = "warn", priority = 10 } -test-dummy-unstable = { level = "forbid", priority = -1 } +im_a_teapot = { level = "warn", priority = 10 } +test_dummy_unstable = { level = "forbid", priority = -1 } [package] name = "foo" @@ -992,7 +991,7 @@ fn dont_always_inherit_workspace_lints() { members = ["foo"] [workspace.lints.cargo] -im-a-teapot = "warn" +im_a_teapot = "warn" "#, ) .file( @@ -1038,7 +1037,7 @@ edition = "2021" baz = { version = "0.1.0", optional = true } [lints.cargo] -implicit-features = "warn" +implicit_features = "warn" "#, ) .file("src/lib.rs", "") @@ -1056,7 +1055,7 @@ edition = "2021" bar = "0.1.0" [lints.cargo] -implicit-features = "warn" +implicit_features = "warn" "#, ) .file("src/lib.rs", "") @@ -1091,7 +1090,7 @@ edition = "2015" authors = [] [lints.cargo] -im-a-teapot = "warn" +im_a_teapot = "warn" "#, ) .file("src/lib.rs", "") @@ -1102,10 +1101,10 @@ im-a-teapot = "warn" .with_status(101) .with_stderr( "\ -error: use of unstable lint `im-a-teapot` +error: use of unstable lint `im_a_teapot` --> Cargo.toml:9:1 | -9 | im-a-teapot = \"warn\" +9 | im_a_teapot = \"warn\" | ^^^^^^^^^^^ this is behind `test-dummy-unstable`, which is not enabled | = help: consider adding `cargo-features = [\"test-dummy-unstable\"]` to the top of the manifest @@ -1125,8 +1124,8 @@ fn check_feature_gated_workspace() { members = ["foo"] [workspace.lints.cargo] -im-a-teapot = { level = "warn", priority = 10 } -test-dummy-unstable = { level = "forbid", priority = -1 } +im_a_teapot = { level = "warn", priority = 10 } +test_dummy_unstable = { level = "forbid", priority = -1 } "#, ) .file( @@ -1150,26 +1149,26 @@ workspace = true .with_status(101) .with_stderr( "\ -error: use of unstable lint `im-a-teapot` +error: use of unstable lint `im_a_teapot` --> Cargo.toml:6:1 | -6 | im-a-teapot = { level = \"warn\", priority = 10 } +6 | im_a_teapot = { level = \"warn\", priority = 10 } | ^^^^^^^^^^^ this is behind `test-dummy-unstable`, which is not enabled | -note: `cargo::im-a-teapot` was inherited +note: `cargo::im_a_teapot` was inherited --> foo/Cargo.toml:9:1 | 9 | workspace = true | ---------------- | = help: consider adding `cargo-features = [\"test-dummy-unstable\"]` to the top of the manifest -error: use of unstable lint `test-dummy-unstable` +error: use of unstable lint `test_dummy_unstable` --> Cargo.toml:7:1 | -7 | test-dummy-unstable = { level = \"forbid\", priority = -1 } +7 | test_dummy_unstable = { level = \"forbid\", priority = -1 } | ^^^^^^^^^^^^^^^^^^^ this is behind `test-dummy-unstable`, which is not enabled | -note: `cargo::test-dummy-unstable` was inherited +note: `cargo::test_dummy_unstable` was inherited --> foo/Cargo.toml:9:1 | 9 | workspace = true