From 6cd7e714822a358d31b712788d3c67e6ba510286 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Sat, 15 Nov 2025 10:42:12 -0500 Subject: [PATCH 1/5] test(lint): show current behavior for version requirements --- .../lints/implicit_minimum_version_req.rs | 1214 +++++++++++++++++ tests/testsuite/lints/mod.rs | 1 + 2 files changed, 1215 insertions(+) create mode 100644 tests/testsuite/lints/implicit_minimum_version_req.rs diff --git a/tests/testsuite/lints/implicit_minimum_version_req.rs b/tests/testsuite/lints/implicit_minimum_version_req.rs new file mode 100644 index 00000000000..8b7f5c887c6 --- /dev/null +++ b/tests/testsuite/lints/implicit_minimum_version_req.rs @@ -0,0 +1,1214 @@ +//! Tests for the `implicit_minimum_version_req` lint. + +use crate::prelude::*; + +use cargo_test_support::basic_manifest; +use cargo_test_support::git; +use cargo_test_support::project; +use cargo_test_support::registry::Package; +use cargo_test_support::str; + +#[cargo_test] +fn major_only() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +dep = "1" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn major_minor() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +dep = "1.0" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn fully_specified_should_not_warn() { + Package::new("dep", "1.2.3").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +dep = "1.0.0" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.2.3 (registry `dummy-registry`) +[CHECKING] dep v1.2.3 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn detailed_dep_major_only() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +dep = { version = "1" } + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn greater_eq() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +dep = ">=1.0" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn less() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +dep = "<2.0" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn wildcard_should_not_warn() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +dep = "1.*" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn wildcard_minor_should_not_warn() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +dep = "1.0.*" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn greater_should_not_warn() { + Package::new("dep", "1.1.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +dep = ">1.0" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.1.0 (registry `dummy-registry`) +[CHECKING] dep v1.1.0 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn less_eq_should_not_warn() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +dep = "<=2.0" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn multiple_requirements() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +dep = ">=1.0, <2.0" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn tilde_requirement_should_not_warn() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +dep = "~1.0" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn exact_requirement_should_not_warn() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +dep = "=1" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn path_dep_should_not_warn() { + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +bar = { path = "bar" } + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" +[package] +name = "bar" +version = "0.1.0" +edition = "2021" +"#, + ) + .file("bar/src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[LOCKING] 1 package to latest compatible version +[CHECKING] bar v0.1.0 ([ROOT]/foo/bar) +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn path_dep_with_registry_version() { + Package::new("bar", "1.0.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +bar = { path = "bar", version = "0.1" } + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" +[package] +name = "bar" +version = "0.1.0" +edition = "2021" +"#, + ) + .file("bar/src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[LOCKING] 1 package to latest compatible version +[CHECKING] bar v0.1.0 ([ROOT]/foo/bar) +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn git_dep_should_not_warn() { + let git_project = git::new("bar", |project| { + project + .file("Cargo.toml", &basic_manifest("bar", "0.1.0")) + .file("src/lib.rs", "") + }); + + let p = project() + .file( + "Cargo.toml", + &format!( + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +bar = {{ git = '{}' }} + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + git_project.url() + ), + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] git repository `[ROOTURL]/bar` +[LOCKING] 1 package to latest compatible version +[CHECKING] bar v0.1.0 ([ROOTURL]/bar#[..]) +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn git_dep_with_registry_version() { + let git_project = git::new("bar", |project| { + project + .file("Cargo.toml", &basic_manifest("bar", "0.1.0")) + .file("src/lib.rs", "") + }); + + let p = project() + .file( + "Cargo.toml", + &format!( + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +bar = {{ git = '{}', version = "0.1" }} + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + git_project.url() + ), + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] git repository `[ROOTURL]/bar` +[LOCKING] 1 package to latest compatible version +[CHECKING] bar v0.1.0 ([ROOTURL]/bar#[..]) +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn dev_dep() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dev-dependencies] +dep = "1" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn build_dep() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[build-dependencies] +dep = "1.0" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .file("build.rs", "fn main() {}") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[COMPILING] dep v1.0.0 +[COMPILING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn target_dep() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +# Spaces are critical here to check Cargo tolerates them +[target.'cfg( all( ) )'.dependencies] +dep = "1" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:11:1 + | +11 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn target_dev_dep() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +# Spaces are critical here to check Cargo tolerates them +[target.'cfg( all( ) )'.dev-dependencies] +dep = "1" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:11:1 + | +11 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn multiple_imprecise_deps() { + Package::new("dep", "1.0.0").publish(); + Package::new("regex", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +dep = "1" +regex = "1.0" + +[lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data( + str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:11:1 + | +11 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 2 packages to latest compatible versions +[DOWNLOADING] crates ... +[DOWNLOADED] regex v1.0.0 (registry `dummy-registry`) +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] regex v1.0.0 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]] + .unordered(), + ) + .run(); +} + +#[cargo_test] +fn workspace_inherited() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[workspace] +members = ["member"] +resolver = "2" + +[workspace.dependencies] +dep = "1" + +[workspace.lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file( + "member/Cargo.toml", + r#" +[package] +name = "member" +edition = "2021" + +[dependencies] +dep.workspace = true + +[lints] +workspace = true +"#, + ) + .file("member/src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[NOTE] `cargo::implicit_minimum_version_req` was inherited + --> member/Cargo.toml:10:1 + | +10 | workspace = true + | ---------------- +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] member v0.0.0 ([ROOT]/foo/member) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn unused_workspace_dep() { + // Should still warn for workspace dep + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[workspace] +members = ["member"] +resolver = "2" + +[workspace.dependencies] +dep = "1" + +[workspace.lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file( + "member/Cargo.toml", + r#" +[package] +name = "member" +edition = "2021" +"#, + ) + .file("member/src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[CHECKING] member v0.0.0 ([ROOT]/foo/member) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn unused_workspace_dep_and_package_implicit_req() { + // Should warn package and workspace separately + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[workspace] +members = ["member"] +resolver = "2" + +[workspace.dependencies] +dep = "1" + +[workspace.lints.cargo] +implicit_minimum_version_req = "warn" +"#, + ) + .file( + "member/Cargo.toml", + r#" +[package] +name = "member" +edition = "2021" + +[dependencies] +dep = "1.0" + +[lints] +workspace = true +"#, + ) + .file("member/src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "warn" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[NOTE] `cargo::implicit_minimum_version_req` was inherited + --> member/Cargo.toml:10:1 + | +10 | workspace = true + | ---------------- +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] member v0.0.0 ([ROOT]/foo/member) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn deny() { + Package::new("dep", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +edition = "2021" + +[dependencies] +dep = "1" + +[lints.cargo] +implicit_minimum_version_req = "deny" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unknown lint: `implicit_minimum_version_req` + --> Cargo.toml:10:1 + | +10 | implicit_minimum_version_req = "deny" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) +[CHECKING] dep v1.0.0 +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} diff --git a/tests/testsuite/lints/mod.rs b/tests/testsuite/lints/mod.rs index 1cb1a3cc9f2..a69da32155e 100644 --- a/tests/testsuite/lints/mod.rs +++ b/tests/testsuite/lints/mod.rs @@ -5,6 +5,7 @@ use cargo_test_support::str; mod blanket_hint_mostly_unused; mod error; +mod implicit_minimum_version_req; mod inherited; mod unknown_lints; mod warning; From 5e1f986afbbfddca41f27cdefb4d69e388b6b229 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Sat, 6 Dec 2025 13:31:29 -0500 Subject: [PATCH 2/5] refactor(lints): move to a module --- src/cargo/util/{lints.rs => lints/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/cargo/util/{lints.rs => lints/mod.rs} (100%) diff --git a/src/cargo/util/lints.rs b/src/cargo/util/lints/mod.rs similarity index 100% rename from src/cargo/util/lints.rs rename to src/cargo/util/lints/mod.rs From 192475a6f54764c8efdda3b8b67fed91a1733bfe Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Sat, 6 Dec 2025 13:56:57 -0500 Subject: [PATCH 3/5] feat(lint): new `implicit_minimum_version_req` lint Add a new `cargo::implicit_minimum_version_req` lint: Only check if dependency has a single caret requirement. All other requirements (multiple, tilde, wildcard) are not linted by this rule, as they usually have significance on what version fields get specified. This currently lints only dependencies with no workspace inheritance. --- src/cargo/core/workspace.rs | 8 +- .../lints/implicit_minimum_version_req.rs | 259 +++++++++++++ src/cargo/util/lints/mod.rs | 10 +- src/doc/src/reference/lints.md | 54 +++ .../lints/implicit_minimum_version_req.rs | 363 ++++++++---------- 5 files changed, 496 insertions(+), 198 deletions(-) create mode 100644 src/cargo/util/lints/implicit_minimum_version_req.rs diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index 6fd76e42e9e..7f1c42c2503 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -26,9 +26,10 @@ use crate::util::context::FeatureUnification; use crate::util::edit_distance; use crate::util::errors::{CargoResult, ManifestError}; use crate::util::interning::InternedString; -use crate::util::lints::{ - analyze_cargo_lints_table, blanket_hint_mostly_unused, check_im_a_teapot, -}; +use crate::util::lints::analyze_cargo_lints_table; +use crate::util::lints::blanket_hint_mostly_unused; +use crate::util::lints::check_im_a_teapot; +use crate::util::lints::implicit_minimum_version_req; use crate::util::toml::{InheritableFields, read_manifest}; use crate::util::{ Filesystem, GlobalContext, IntoUrl, context::CargoResolverConfig, context::ConfigRelativePath, @@ -1296,6 +1297,7 @@ impl<'gctx> Workspace<'gctx> { self.gctx, )?; check_im_a_teapot(pkg, &path, &cargo_lints, &mut error_count, self.gctx)?; + implicit_minimum_version_req(pkg, &path, &cargo_lints, &mut error_count, self.gctx)?; } if error_count > 0 { diff --git a/src/cargo/util/lints/implicit_minimum_version_req.rs b/src/cargo/util/lints/implicit_minimum_version_req.rs new file mode 100644 index 00000000000..63d0c3ec8f1 --- /dev/null +++ b/src/cargo/util/lints/implicit_minimum_version_req.rs @@ -0,0 +1,259 @@ +use std::collections::HashMap; +use std::path::Path; + +use annotate_snippets::AnnotationKind; +use annotate_snippets::Group; +use annotate_snippets::Level; +use annotate_snippets::Patch; +use annotate_snippets::Snippet; +use cargo_platform::Platform; +use cargo_util_schemas::manifest::TomlToolLints; +use toml::de::DeValue; + +use crate::CargoResult; +use crate::GlobalContext; +use crate::core::Manifest; +use crate::core::Package; +use crate::util::OptVersionReq; +use crate::util::lints::Lint; +use crate::util::lints::LintLevel; +use crate::util::lints::LintLevelReason; +use crate::util::lints::get_key_value; +use crate::util::lints::rel_cwd_manifest_path; + +pub const LINT: Lint = Lint { + name: "implicit_minimum_version_req", + desc: "dependency version requirement without an explicit minimum version", + groups: &[], + default_level: LintLevel::Allow, + edition_lint_opts: None, + feature_gate: None, + docs: Some( + r#" +### What it does + +Checks for dependency version requirements +that do not explicitly specify a full `major.minor.patch` version requirement, +such as `serde = "1"` or `serde = "1.0"`. + +This lint currently only applies to caret requirements +(the [default requirements](specifying-dependencies.md#default-requirements)). + +### Why it is bad + +Version requirements without an explicit full version +can be misleading about the actual minimum supported version. +For example, +`serde = "1"` has an implicit minimum bound of `1.0.0`. +If your code actually requires features from `1.0.219`, +the implicit minimum bound of `1.0.0` gives a false impression about compatibility. + +Specifying the full version helps with: + +- Accurate minimum version documentation +- Better compatibility with `-Z minimal-versions` +- Clearer dependency constraints for consumers + +### Drawbacks + +Even with a fully specified version, +the minimum bound might still be incorrect if untested. +This lint helps make the minimum version requirement explicit +but doesn't guarantee correctness. + +### Example + +```toml +[dependencies] +serde = "1" +``` + +Should be written as a full specific version: + +```toml +[dependencies] +serde = "1.0.219" +``` +"#, + ), +}; + +pub fn implicit_minimum_version_req( + pkg: &Package, + manifest_path: &Path, + cargo_lints: &TomlToolLints, + error_count: &mut usize, + gctx: &GlobalContext, +) -> CargoResult<()> { + let manifest = pkg.manifest(); + let (lint_level, reason) = LINT.level( + cargo_lints, + manifest.edition(), + manifest.unstable_features(), + ); + + if lint_level == LintLevel::Allow { + return Ok(()); + } + + let document = manifest.document(); + let contents = manifest.contents(); + let manifest_path = rel_cwd_manifest_path(manifest_path, gctx); + let target_key_for_platform = target_key_for_platform(&manifest); + + for dep in manifest.dependencies().iter() { + let version_req = dep.version_req(); + let Some(suggested_req) = get_suggested_version_req(&version_req) else { + continue; + }; + + let name_in_toml = dep.name_in_toml().as_str(); + let key_path = + if let Some(cfg) = dep.platform().and_then(|p| target_key_for_platform.get(p)) { + &["target", &cfg, dep.kind().kind_table(), name_in_toml][..] + } else { + &[dep.kind().kind_table(), name_in_toml][..] + }; + + let Some(span) = span_of_version_req(document, key_path) else { + continue; + }; + + let report = report( + lint_level, + reason, + span, + contents, + &manifest_path, + &suggested_req, + ); + + if lint_level.is_error() { + *error_count += 1; + } + gctx.shell().print_report(&report, lint_level.force())?; + } + + Ok(()) +} + +pub fn span_of_version_req<'doc>( + document: &'doc toml::Spanned>, + path: &[&str], +) -> Option> { + let (_key, value) = get_key_value(document, path)?; + + match value.as_ref() { + DeValue::String(_) => Some(value.span()), + DeValue::Table(map) if map.get("workspace").is_some() => { + // We only lint non-workspace-inherited dependencies + None + } + DeValue::Table(map) => { + let Some(v) = map.get("version") else { + panic!("version must be specified or workspace-inherited"); + }; + Some(v.span()) + } + _ => unreachable!("dependency must be string or table"), + } +} + +fn report<'a>( + lint_level: LintLevel, + reason: LintLevelReason, + span: std::ops::Range, + contents: &'a str, + manifest_path: &str, + suggested_req: &str, +) -> [Group<'a>; 2] { + let level = lint_level.to_diagnostic_level(); + let emitted_source = LINT.emitted_source(lint_level, reason); + let replacement = format!(r#""{suggested_req}""#); + let label = "missing full version components"; + let secondary_title = "consider specifying full `major.minor.patch` version components"; + [ + level.clone().primary_title(LINT.desc).element( + Snippet::source(contents) + .path(manifest_path.to_owned()) + .annotation(AnnotationKind::Primary.span(span.clone()).label(label)), + ), + Level::HELP + .secondary_title(secondary_title) + .element(Snippet::source(contents).patch(Patch::new(span.clone(), replacement))) + .element(Level::NOTE.message(emitted_source)), + ] +} + +fn get_suggested_version_req(req: &OptVersionReq) -> Option { + use semver::Op; + let OptVersionReq::Req(req) = req else { + return None; + }; + let mut has_suggestions = false; + let mut comparators = Vec::new(); + + for mut cmp in req.comparators.iter().cloned() { + match cmp.op { + Op::Caret | Op::GreaterEq => { + // Only focus on comparator that has only `major` or `major.minor` + if cmp.minor.is_some() && cmp.patch.is_some() { + comparators.push(cmp); + continue; + } else { + has_suggestions = true; + cmp.minor.get_or_insert(0); + cmp.patch.get_or_insert(0); + comparators.push(cmp); + } + } + Op::Exact | Op::Tilde | Op::Wildcard | Op::Greater | Op::Less | Op::LessEq => { + comparators.push(cmp); + continue; + } + _ => panic!("unknown comparator in `{cmp}`"), + } + } + + if !has_suggestions { + return None; + } + + // This is a lossy suggestion that + // + // * extra spaces are removed + // * caret operator `^` is stripped + let mut suggestion = String::new(); + + for cmp in &comparators { + if !suggestion.is_empty() { + suggestion.push_str(", "); + } + let s = cmp.to_string(); + + if cmp.op == Op::Caret { + suggestion.push_str(s.strip_prefix('^').unwrap_or(&s)); + } else { + suggestion.push_str(&s); + } + } + + Some(suggestion) +} + +/// A map from parsed `Platform` to their original TOML key strings. +/// This is needed for constructing TOML key paths in diagnostics. +/// +/// This is only relevant for package dependencies. +fn target_key_for_platform(manifest: &Manifest) -> HashMap { + manifest + .normalized_toml() + .target + .as_ref() + .map(|map| { + map.keys() + .map(|k| (k.parse().expect("already parsed"), k.clone())) + .collect() + }) + .unwrap_or_default() +} diff --git a/src/cargo/util/lints/mod.rs b/src/cargo/util/lints/mod.rs index abb705b53a8..adeb6b276ad 100644 --- a/src/cargo/util/lints/mod.rs +++ b/src/cargo/util/lints/mod.rs @@ -8,8 +8,16 @@ use std::fmt::Display; use std::ops::Range; use std::path::Path; +mod implicit_minimum_version_req; +pub use implicit_minimum_version_req::implicit_minimum_version_req; + const LINT_GROUPS: &[LintGroup] = &[TEST_DUMMY_UNSTABLE]; -pub const LINTS: &[Lint] = &[BLANKET_HINT_MOSTLY_UNUSED, IM_A_TEAPOT, UNKNOWN_LINTS]; +pub const LINTS: &[Lint] = &[ + BLANKET_HINT_MOSTLY_UNUSED, + implicit_minimum_version_req::LINT, + IM_A_TEAPOT, + UNKNOWN_LINTS, +]; pub fn analyze_cargo_lints_table( pkg: &Package, diff --git a/src/doc/src/reference/lints.md b/src/doc/src/reference/lints.md index 8393efb9ec5..974e44243bc 100644 --- a/src/doc/src/reference/lints.md +++ b/src/doc/src/reference/lints.md @@ -2,6 +2,11 @@ Note: [Cargo's linting system is unstable](unstable.md#lintscargo) and can only be used on nightly toolchains +## Allowed-by-default + +These lints are all set to the 'allow' level by default. +- [`implicit_minimum_version_req`](#implicit_minimum_version_req) + ## Warn-by-default These lints are all set to the 'warn' level by default. @@ -36,6 +41,55 @@ hint-mostly-unused = true ``` +## `implicit_minimum_version_req` +Set to `allow` by default + +### What it does + +Checks for dependency version requirements +that do not explicitly specify a full `major.minor.patch` version requirement, +such as `serde = "1"` or `serde = "1.0"`. + +This lint currently only applies to caret requirements +(the [default requirements](specifying-dependencies.md#default-requirements)). + +### Why it is bad + +Version requirements without an explicit full version +can be misleading about the actual minimum supported version. +For example, +`serde = "1"` has an implicit minimum bound of `1.0.0`. +If your code actually requires features from `1.0.219`, +the implicit minimum bound of `1.0.0` gives a false impression about compatibility. + +Specifying the full version helps with: + +- Accurate minimum version documentation +- Better compatibility with `-Z minimal-versions` +- Clearer dependency constraints for consumers + +### Drawbacks + +Even with a fully specified version, +the minimum bound might still be incorrect if untested. +This lint helps make the minimum version requirement explicit +but doesn't guarantee correctness. + +### Example + +```toml +[dependencies] +serde = "1" +``` + +Should be written as a full specific version: + +```toml +[dependencies] +serde = "1.0.219" +``` + + ## `unknown_lints` Set to `warn` by default diff --git a/tests/testsuite/lints/implicit_minimum_version_req.rs b/tests/testsuite/lints/implicit_minimum_version_req.rs index 8b7f5c887c6..36a3c20d803 100644 --- a/tests/testsuite/lints/implicit_minimum_version_req.rs +++ b/tests/testsuite/lints/implicit_minimum_version_req.rs @@ -33,13 +33,17 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:7:7 + | +7 | dep = "1" + | ^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +7 | dep = "1.0.0" + | ++++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -77,13 +81,17 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:7:7 + | +7 | dep = "1.0" + | ^^^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +7 | dep = "1.0.0" + | ++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -121,13 +129,6 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -165,13 +166,17 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:7:19 + | +7 | dep = { version = "1" } + | ^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +7 | dep = { version = "1.0.0" } + | ++++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -209,13 +214,17 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:7:7 + | +7 | dep = ">=1.0" + | ^^^^^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +7 | dep = ">=1.0.0" + | ++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -229,7 +238,7 @@ implicit_minimum_version_req = "warn" } #[cargo_test] -fn less() { +fn less_should_not_warn() { Package::new("dep", "1.0.0").publish(); let p = project() @@ -253,13 +262,6 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -297,13 +299,6 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -341,13 +336,6 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -385,13 +373,6 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -429,13 +410,6 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -473,13 +447,17 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:7:7 + | +7 | dep = ">=1.0, <2.0" + | ^^^^^^^^^^^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +7 | dep = ">=1.0.0, <2.0" + | ++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -517,13 +495,6 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -561,13 +532,6 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -613,13 +577,6 @@ edition = "2021" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default [LOCKING] 1 package to latest compatible version [CHECKING] bar v0.1.0 ([ROOT]/foo/bar) [CHECKING] foo v0.0.0 ([ROOT]/foo) @@ -663,13 +620,17 @@ edition = "2021" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:7:33 + | +7 | bar = { path = "bar", version = "0.1" } + | ^^^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +7 | bar = { path = "bar", version = "0.1.0" } + | ++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [LOCKING] 1 package to latest compatible version [CHECKING] bar v0.1.0 ([ROOT]/foo/bar) [CHECKING] foo v0.0.0 ([ROOT]/foo) @@ -711,13 +672,6 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default [UPDATING] git repository `[ROOTURL]/bar` [LOCKING] 1 package to latest compatible version [CHECKING] bar v0.1.0 ([ROOTURL]/bar#[..]) @@ -760,13 +714,17 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:7:[..] + | +7 | bar = { git = '[ROOTURL]/bar', version = "0.1" } + | [..]^^^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +7 | bar = { git = '[ROOTURL]/bar', version = "0.1.0" } + | [..]++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [UPDATING] git repository `[ROOTURL]/bar` [LOCKING] 1 package to latest compatible version [CHECKING] bar v0.1.0 ([ROOTURL]/bar#[..]) @@ -802,13 +760,17 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:7:7 + | +7 | dep = "1" + | ^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +7 | dep = "1.0.0" + | ++++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [CHECKING] foo v0.0.0 ([ROOT]/foo) @@ -844,13 +806,17 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:7:7 + | +7 | dep = "1.0" + | ^^^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +7 | dep = "1.0.0" + | ++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -889,13 +855,17 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:11:1 - | -11 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:8:7 + | +8 | dep = "1" + | ^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +8 | dep = "1.0.0" + | ++++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -934,13 +904,17 @@ implicit_minimum_version_req = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:11:1 - | -11 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:8:7 + | +8 | dep = "1" + | ^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +8 | dep = "1.0.0" + | ++++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [CHECKING] foo v0.0.0 ([ROOT]/foo) @@ -951,7 +925,7 @@ implicit_minimum_version_req = "warn" } #[cargo_test] -fn multiple_imprecise_deps() { +fn multiple_implicit_deps() { Package::new("dep", "1.0.0").publish(); Package::new("regex", "1.0.0").publish(); @@ -978,13 +952,28 @@ implicit_minimum_version_req = "warn" .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data( str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:11:1 - | -11 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:7:7 + | +7 | dep = "1" + | ^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +7 | dep = "1.0.0" + | ++++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:8:9 + | +8 | regex = "1.0" + | ^^^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +8 | regex = "1.0.0" + | ++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 2 packages to latest compatible versions [DOWNLOADING] crates ... @@ -1040,18 +1029,6 @@ workspace = true p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default -[NOTE] `cargo::implicit_minimum_version_req` was inherited - --> member/Cargo.toml:10:1 - | -10 | workspace = true - | ---------------- [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -1145,18 +1122,17 @@ workspace = true p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "warn" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default -[NOTE] `cargo::implicit_minimum_version_req` was inherited - --> member/Cargo.toml:10:1 - | -10 | workspace = true - | ---------------- +[WARNING] dependency version requirement without an explicit minimum version + --> member/Cargo.toml:7:7 + | +7 | dep = "1.0" + | ^^^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +7 | dep = "1.0.0" + | ++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -1193,21 +1169,20 @@ implicit_minimum_version_req = "deny" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_status(101) .with_stderr_data(str![[r#" -[WARNING] unknown lint: `implicit_minimum_version_req` - --> Cargo.toml:10:1 - | -10 | implicit_minimum_version_req = "deny" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = [NOTE] `cargo::unknown_lints` is set to `warn` by default -[UPDATING] `dummy-registry` index -[LOCKING] 1 package to latest compatible version -[DOWNLOADING] crates ... -[DOWNLOADED] dep v1.0.0 (registry `dummy-registry`) -[CHECKING] dep v1.0.0 -[CHECKING] foo v0.0.0 ([ROOT]/foo) -[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +[ERROR] dependency version requirement without an explicit minimum version + --> Cargo.toml:7:7 + | +7 | dep = "1" + | ^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +7 | dep = "1.0.0" + | ++++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `deny` in `[lints]` +[ERROR] encountered 1 error while running lints "#]]) .run(); From 1464f97bc5ba0e9a90a50b395115c86f34c7945d Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Mon, 8 Dec 2025 16:35:08 -0500 Subject: [PATCH 4/5] refactor(lint): wrapper for lints against both pkg and workspace --- src/cargo/core/workspace.rs | 8 ++++- .../lints/implicit_minimum_version_req.rs | 30 ++++++++++++++----- src/cargo/util/lints/mod.rs | 24 +++++++++++++++ 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index 7f1c42c2503..27baa06112a 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -1297,7 +1297,13 @@ impl<'gctx> Workspace<'gctx> { self.gctx, )?; check_im_a_teapot(pkg, &path, &cargo_lints, &mut error_count, self.gctx)?; - implicit_minimum_version_req(pkg, &path, &cargo_lints, &mut error_count, self.gctx)?; + implicit_minimum_version_req( + pkg.into(), + &path, + &cargo_lints, + &mut error_count, + self.gctx, + )?; } if error_count > 0 { diff --git a/src/cargo/util/lints/implicit_minimum_version_req.rs b/src/cargo/util/lints/implicit_minimum_version_req.rs index 63d0c3ec8f1..a56f3eae9b9 100644 --- a/src/cargo/util/lints/implicit_minimum_version_req.rs +++ b/src/cargo/util/lints/implicit_minimum_version_req.rs @@ -18,6 +18,7 @@ use crate::util::OptVersionReq; use crate::util::lints::Lint; use crate::util::lints::LintLevel; use crate::util::lints::LintLevelReason; +use crate::util::lints::ManifestFor; use crate::util::lints::get_key_value; use crate::util::lints::rel_cwd_manifest_path; @@ -79,26 +80,39 @@ serde = "1.0.219" }; pub fn implicit_minimum_version_req( - pkg: &Package, + manifest: ManifestFor<'_>, manifest_path: &Path, cargo_lints: &TomlToolLints, error_count: &mut usize, gctx: &GlobalContext, ) -> CargoResult<()> { - let manifest = pkg.manifest(); - let (lint_level, reason) = LINT.level( - cargo_lints, - manifest.edition(), - manifest.unstable_features(), - ); + let (lint_level, reason) = manifest.lint_level(cargo_lints, LINT); if lint_level == LintLevel::Allow { return Ok(()); } + let manifest_path = rel_cwd_manifest_path(manifest_path, gctx); + + match manifest { + ManifestFor::Package(pkg) => { + lint_package(pkg, manifest_path, lint_level, reason, error_count, gctx) + } + } +} + +pub fn lint_package( + pkg: &Package, + manifest_path: String, + lint_level: LintLevel, + reason: LintLevelReason, + error_count: &mut usize, + gctx: &GlobalContext, +) -> CargoResult<()> { + let manifest = pkg.manifest(); + let document = manifest.document(); let contents = manifest.contents(); - let manifest_path = rel_cwd_manifest_path(manifest_path, gctx); let target_key_for_platform = target_key_for_platform(&manifest); for dep in manifest.dependencies().iter() { diff --git a/src/cargo/util/lints/mod.rs b/src/cargo/util/lints/mod.rs index adeb6b276ad..b8bdca09bef 100644 --- a/src/cargo/util/lints/mod.rs +++ b/src/cargo/util/lints/mod.rs @@ -19,6 +19,30 @@ pub const LINTS: &[Lint] = &[ UNKNOWN_LINTS, ]; +/// Scope at which a lint runs: package-level or workspace-level. +pub enum ManifestFor<'a> { + /// Lint runs for a specific package. + Package(&'a Package), +} + +impl ManifestFor<'_> { + fn lint_level(&self, pkg_lints: &TomlToolLints, lint: Lint) -> (LintLevel, LintLevelReason) { + match self { + ManifestFor::Package(p) => lint.level( + pkg_lints, + p.manifest().edition(), + p.manifest().unstable_features(), + ), + } + } +} + +impl<'a> From<&'a Package> for ManifestFor<'a> { + fn from(value: &'a Package) -> ManifestFor<'a> { + ManifestFor::Package(value) + } +} + pub fn analyze_cargo_lints_table( pkg: &Package, path: &Path, From 864bf9677f06d851bf35e793a0126548eaeff394 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Mon, 8 Dec 2025 16:56:20 -0500 Subject: [PATCH 5/5] feat(lint): lint also workspace dependencies This has a future performance that version requirments in `[workspace.dependencies]` shoud avoid reparse --- src/cargo/core/workspace.rs | 7 ++ .../lints/implicit_minimum_version_req.rs | 76 +++++++++++++++++++ src/cargo/util/lints/mod.rs | 9 +++ .../lints/implicit_minimum_version_req.rs | 33 ++++++++ 4 files changed, 125 insertions(+) diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index 27baa06112a..a0bd9367d4b 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -1340,6 +1340,13 @@ impl<'gctx> Workspace<'gctx> { if self.gctx.cli_unstable().cargo_lints { // Calls to lint functions go in here + implicit_minimum_version_req( + self.root_maybe().into(), + self.root_manifest(), + &cargo_lints, + &mut error_count, + self.gctx, + )?; } // This is a short term hack to allow `blanket_hint_mostly_unused` diff --git a/src/cargo/util/lints/implicit_minimum_version_req.rs b/src/cargo/util/lints/implicit_minimum_version_req.rs index a56f3eae9b9..39825e00434 100644 --- a/src/cargo/util/lints/implicit_minimum_version_req.rs +++ b/src/cargo/util/lints/implicit_minimum_version_req.rs @@ -7,12 +7,14 @@ use annotate_snippets::Level; use annotate_snippets::Patch; use annotate_snippets::Snippet; use cargo_platform::Platform; +use cargo_util_schemas::manifest::TomlDependency; use cargo_util_schemas::manifest::TomlToolLints; use toml::de::DeValue; use crate::CargoResult; use crate::GlobalContext; use crate::core::Manifest; +use crate::core::MaybePackage; use crate::core::Package; use crate::util::OptVersionReq; use crate::util::lints::Lint; @@ -98,6 +100,14 @@ pub fn implicit_minimum_version_req( ManifestFor::Package(pkg) => { lint_package(pkg, manifest_path, lint_level, reason, error_count, gctx) } + ManifestFor::Workspace(maybe_pkg) => lint_workspace( + maybe_pkg, + manifest_path, + lint_level, + reason, + error_count, + gctx, + ), } } @@ -151,6 +161,72 @@ pub fn lint_package( Ok(()) } +pub fn lint_workspace( + maybe_pkg: &MaybePackage, + manifest_path: String, + lint_level: LintLevel, + reason: LintLevelReason, + error_count: &mut usize, + gctx: &GlobalContext, +) -> CargoResult<()> { + let document = maybe_pkg.document(); + let contents = maybe_pkg.contents(); + let toml = match maybe_pkg { + MaybePackage::Package(p) => p.manifest().normalized_toml(), + MaybePackage::Virtual(vm) => vm.normalized_toml(), + }; + let dep_iter = toml + .workspace + .as_ref() + .and_then(|ws| ws.dependencies.as_ref()) + .into_iter() + .flat_map(|deps| deps.iter()) + .map(|(name, dep)| { + let name = name.as_str(); + let ver = match dep { + TomlDependency::Simple(ver) => ver, + TomlDependency::Detailed(detailed) => { + let Some(ver) = detailed.version.as_ref() else { + return (name, OptVersionReq::Any); + }; + ver + } + }; + let req = semver::VersionReq::parse(ver) + .map(Into::into) + .unwrap_or(OptVersionReq::Any); + (name, req) + }); + + for (name_in_toml, version_req) in dep_iter { + let Some(suggested_req) = get_suggested_version_req(&version_req) else { + continue; + }; + + let key_path = ["workspace", "dependencies", name_in_toml]; + + let Some(span) = span_of_version_req(document, &key_path) else { + continue; + }; + + let report = report( + lint_level, + reason, + span, + contents, + &manifest_path, + &suggested_req, + ); + + if lint_level.is_error() { + *error_count += 1; + } + gctx.shell().print_report(&report, lint_level.force())?; + } + + Ok(()) +} + pub fn span_of_version_req<'doc>( document: &'doc toml::Spanned>, path: &[&str], diff --git a/src/cargo/util/lints/mod.rs b/src/cargo/util/lints/mod.rs index b8bdca09bef..39cb38af525 100644 --- a/src/cargo/util/lints/mod.rs +++ b/src/cargo/util/lints/mod.rs @@ -23,6 +23,8 @@ pub const LINTS: &[Lint] = &[ pub enum ManifestFor<'a> { /// Lint runs for a specific package. Package(&'a Package), + /// Lint runs for workspace-level config. + Workspace(&'a MaybePackage), } impl ManifestFor<'_> { @@ -33,6 +35,7 @@ impl ManifestFor<'_> { p.manifest().edition(), p.manifest().unstable_features(), ), + ManifestFor::Workspace(p) => lint.level(pkg_lints, p.edition(), p.unstable_features()), } } } @@ -43,6 +46,12 @@ impl<'a> From<&'a Package> for ManifestFor<'a> { } } +impl<'a> From<&'a MaybePackage> for ManifestFor<'a> { + fn from(value: &'a MaybePackage) -> ManifestFor<'a> { + ManifestFor::Workspace(value) + } +} + pub fn analyze_cargo_lints_table( pkg: &Package, path: &Path, diff --git a/tests/testsuite/lints/implicit_minimum_version_req.rs b/tests/testsuite/lints/implicit_minimum_version_req.rs index 36a3c20d803..80044e20f31 100644 --- a/tests/testsuite/lints/implicit_minimum_version_req.rs +++ b/tests/testsuite/lints/implicit_minimum_version_req.rs @@ -1029,6 +1029,17 @@ workspace = true p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:7:7 + | +7 | dep = "1" + | ^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +7 | dep = "1.0.0" + | ++++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -1075,6 +1086,17 @@ edition = "2021" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:7:7 + | +7 | dep = "1" + | ^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +7 | dep = "1.0.0" + | ++++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [CHECKING] member v0.0.0 ([ROOT]/foo/member) [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s @@ -1122,6 +1144,17 @@ workspace = true p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" +[WARNING] dependency version requirement without an explicit minimum version + --> Cargo.toml:7:7 + | +7 | dep = "1" + | ^^^ missing full version components + | +[HELP] consider specifying full `major.minor.patch` version components + | +7 | dep = "1.0.0" + | ++++ + = [NOTE] `cargo::implicit_minimum_version_req` is set to `warn` in `[lints]` [WARNING] dependency version requirement without an explicit minimum version --> member/Cargo.toml:7:7 |