From 31d38fbf17831266936588f5e2671c4dc4169cba Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Sat, 15 Nov 2025 10:42:12 -0500 Subject: [PATCH 1/2] test(lint): show current behavior for version requirements --- .../lints/imprecise_version_requirements.rs | 750 ++++++++++++++++++ tests/testsuite/lints/mod.rs | 1 + 2 files changed, 751 insertions(+) create mode 100644 tests/testsuite/lints/imprecise_version_requirements.rs diff --git a/tests/testsuite/lints/imprecise_version_requirements.rs b/tests/testsuite/lints/imprecise_version_requirements.rs new file mode 100644 index 00000000000..47ad87eeedc --- /dev/null +++ b/tests/testsuite/lints/imprecise_version_requirements.rs @@ -0,0 +1,750 @@ +//! Tests for the `imprecise_version_requirements` lint. + +use crate::prelude::*; +use cargo_test_support::git; +use cargo_test_support::registry::Package; +use cargo_test_support::str; +use cargo_test_support::{basic_manifest, project}; + +#[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] +imprecise_version_requirements = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[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] +imprecise_version_requirements = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[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] +imprecise_version_requirements = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[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] +imprecise_version_requirements = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[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] +imprecise_version_requirements = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[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] +imprecise_version_requirements = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[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_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, <2.0" + +[lints.cargo] +imprecise_version_requirements = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[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, <2.0" + +[lints.cargo] +imprecise_version_requirements = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[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] +imprecise_version_requirements = "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#" +[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] +imprecise_version_requirements = "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#" +[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] +imprecise_version_requirements = "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#" +[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] +imprecise_version_requirements = "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#" +[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] +imprecise_version_requirements = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[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] +imprecise_version_requirements = "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#" +[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( unix )'.dependencies] +dep = "1" + +[lints.cargo] +imprecise_version_requirements = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[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( unix )'.dev-dependencies] +dep = "1" + +[lints.cargo] +imprecise_version_requirements = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[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] +imprecise_version_requirements = "warn" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data( + str![[r#" +[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] +imprecise_version_requirements = "warn" +"#, + ) + .file( + "member/Cargo.toml", + r#" +[package] +name = "member" +version = "0.1.0" +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#" +[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.1.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] +imprecise_version_requirements = "deny" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[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..ca4ef04d853 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 imprecise_version_requirements; mod inherited; mod unknown_lints; mod warning; From 7fac5204ed6bf9f3d4a21452794eae3984acd0d6 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Mon, 1 Dec 2025 12:55:57 -0500 Subject: [PATCH 2/2] feat(lint): imprecise_version_requirements Add a new `cargo::imprecise_version_requirements` lint: Only check if dependency has a single caret requirement. All other requirements (multiple, tilde, wildcard) are not linted by this rule. --- src/cargo/core/workspace.rs | 17 +- src/cargo/util/lints.rs | 203 +++++++++++++++++- src/doc/src/reference/lints.md | 48 +++++ .../lints/imprecise_version_requirements.rs | 105 ++++++++- 4 files changed, 361 insertions(+), 12 deletions(-) diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index 8035ad74265..581294fc8e2 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::imprecise_version_requirements; use crate::util::toml::{InheritableFields, read_manifest}; use crate::util::{ Filesystem, GlobalContext, IntoUrl, context::CargoResolverConfig, context::ConfigRelativePath, @@ -1296,6 +1297,16 @@ impl<'gctx> Workspace<'gctx> { self.gctx, )?; check_im_a_teapot(pkg, &path, &cargo_lints, &mut error_count, self.gctx)?; + imprecise_version_requirements( + pkg, + &path, + &cargo_lints, + ws_contents, + ws_document, + self.root_manifest(), + &mut error_count, + self.gctx, + )?; } if error_count > 0 { diff --git a/src/cargo/util/lints.rs b/src/cargo/util/lints.rs index abb705b53a8..de7e9f190a3 100644 --- a/src/cargo/util/lints.rs +++ b/src/cargo/util/lints.rs @@ -1,15 +1,31 @@ use crate::core::{Edition, Feature, Features, Manifest, MaybePackage, Package}; use crate::{CargoResult, GlobalContext}; + use annotate_snippets::{AnnotationKind, Group, Level, Patch, Snippet}; -use cargo_util_schemas::manifest::{ProfilePackageSpec, TomlLintLevel, TomlToolLints}; +use cargo_util_schemas::manifest::ProfilePackageSpec; +use cargo_util_schemas::manifest::TomlLintLevel; +use cargo_util_schemas::manifest::TomlToolLints; use pathdiff::diff_paths; + use std::borrow::Cow; +use std::collections::HashMap; use std::fmt::Display; use std::ops::Range; use std::path::Path; 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, + IMPRECISE_VERSION_REQUIREMENTS, + IM_A_TEAPOT, + UNKNOWN_LINTS, +]; + +enum SpanOrigin { + Specified(core::ops::Range), + Inherited(core::ops::Range), +} pub fn analyze_cargo_lints_table( pkg: &Package, @@ -743,6 +759,189 @@ fn output_unknown_lints( Ok(()) } +const IMPRECISE_VERSION_REQUIREMENTS: Lint = Lint { + name: "imprecise_version_requirements", + desc: "dependency version requirement lacks full precision", + groups: &[], + default_level: LintLevel::Allow, + edition_lint_opts: None, + feature_gate: None, + docs: Some( + r#" +### What it does + +Checks for dependency version requirements that lack full `major.minor.patch` precision, +such as `serde = "1"` or `serde = "1.0"`. + +### Why it is bad + +Imprecise version requirements can be misleading about the actual minimum supported version. +For example, +`serde = "1"` suggests that any version from `1.0.0` onwards is acceptable, +but if your code actually requires features from `1.0.219`, +the imprecise requirement 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 fully specified versions, +the minimum bound might still be incorrect if untested. +This lint helps improve precision 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 imprecise_version_requirements( + pkg: &Package, + path: &Path, + pkg_lints: &TomlToolLints, + ws_contents: &str, + ws_document: &toml::Spanned>, + ws_path: &Path, + error_count: &mut usize, + gctx: &GlobalContext, +) -> CargoResult<()> { + let manifest = pkg.manifest(); + let (lint_level, reason) = IMPRECISE_VERSION_REQUIREMENTS.level( + pkg_lints, + manifest.edition(), + manifest.unstable_features(), + ); + + if lint_level == LintLevel::Allow { + return Ok(()); + } + + let manifest_path = rel_cwd_manifest_path(path, gctx); + + let platform_map: HashMap = manifest + .normalized_toml() + .target + .as_ref() + .map(|map| { + map.keys() + .map(|k| (k.parse().expect("already parsed"), k.clone())) + .collect() + }) + .unwrap_or_default(); + + for dep in manifest.dependencies().iter() { + let crate::util::OptVersionReq::Req(req) = dep.version_req() else { + continue; + }; + let [cmp] = req.comparators.as_slice() else { + continue; + }; + if cmp.op != semver::Op::Caret { + continue; + } + if cmp.minor.is_some() && cmp.patch.is_some() { + continue; + } + + // Only focus on single caret requirement that has only `major` or `major.minor` + let name_in_toml = dep.name_in_toml().as_str(); + + let key_path = if let Some(cfg) = dep.platform().and_then(|p| platform_map.get(p)) { + &["target", &cfg, dep.kind().kind_table(), name_in_toml][..] + } else { + &[dep.kind().kind_table(), name_in_toml][..] + }; + + let Some((_key, value)) = get_key_value(manifest.document(), key_path) else { + continue; + }; + + let span = match value.as_ref() { + toml::de::DeValue::String(_) => SpanOrigin::Specified(value.span()), + toml::de::DeValue::Table(map) => { + if let Some(v) = map.get("version").filter(|v| v.as_ref().is_str()) { + SpanOrigin::Specified(v.span()) + } else if let Some((k, v)) = map + .get_key_value("workspace") + .filter(|(_, v)| v.as_ref().is_bool()) + { + SpanOrigin::Inherited(k.span().start..v.span().end) + } else { + panic!("version must be specified or workspace-inherited"); + } + } + _ => unreachable!("dependency must be string or table"), + }; + + let level = lint_level.to_diagnostic_level(); + let title = IMPRECISE_VERSION_REQUIREMENTS.desc; + let emitted_source = IMPRECISE_VERSION_REQUIREMENTS.emitted_source(lint_level, reason); + let report = match span { + SpanOrigin::Specified(span) => &[Group::with_title(level.clone().primary_title(title)) + .element( + Snippet::source(manifest.contents()) + .path(&manifest_path) + .annotation(AnnotationKind::Primary.span(span)), + ) + .element(Level::NOTE.message(emitted_source))][..], + SpanOrigin::Inherited(inherit_span) => { + let key_path = &["workspace", "dependencies", name_in_toml]; + let (_, value) = + get_key_value(ws_document, key_path).expect("must have workspace dep"); + let ws_span = match value.as_ref() { + toml::de::DeValue::String(_) => value.span(), + toml::de::DeValue::Table(map) => map + .get("version") + .filter(|v| v.as_ref().is_str()) + .map(|v| v.span()) + .expect("must have a version field"), + _ => unreachable!("dependency must be string or table"), + }; + + let ws_path = rel_cwd_manifest_path(ws_path, gctx); + let second_title = format!("dependency `{name_in_toml}` was inherited"); + + &[ + Group::with_title(level.clone().primary_title(title)).element( + Snippet::source(ws_contents) + .path(ws_path) + .annotation(AnnotationKind::Primary.span(ws_span)), + ), + Group::with_title(Level::NOTE.secondary_title(second_title)) + .element( + Snippet::source(manifest.contents()) + .path(&manifest_path) + .annotation(AnnotationKind::Context.span(inherit_span)), + ) + .element(Level::NOTE.message(emitted_source)), + ][..] + } + }; + + if lint_level.is_error() { + *error_count += 1; + } + gctx.shell().print_report(report, lint_level.force())?; + } + + Ok(()) +} + #[cfg(test)] mod tests { use itertools::Itertools; diff --git a/src/doc/src/reference/lints.md b/src/doc/src/reference/lints.md index 8393efb9ec5..f8c624cf38a 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. +- [`imprecise_version_requirements`](#imprecise_version_requirements) + ## Warn-by-default These lints are all set to the 'warn' level by default. @@ -36,6 +41,49 @@ hint-mostly-unused = true ``` +## `imprecise_version_requirements` +Set to `allow` by default + +### What it does + +Checks for dependency version requirements that lack full `major.minor.patch` precision, +such as `serde = "1"` or `serde = "1.0"`. + +### Why it is bad + +Imprecise version requirements can be misleading about the actual minimum supported version. +For example, +`serde = "1"` suggests that any version from `1.0.0` onwards is acceptable, +but if your code actually requires features from `1.0.219`, +the imprecise requirement 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 fully specified versions, +the minimum bound might still be incorrect if untested. +This lint helps improve precision 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/imprecise_version_requirements.rs b/tests/testsuite/lints/imprecise_version_requirements.rs index 47ad87eeedc..9a7f41ed376 100644 --- a/tests/testsuite/lints/imprecise_version_requirements.rs +++ b/tests/testsuite/lints/imprecise_version_requirements.rs @@ -31,6 +31,13 @@ imprecise_version_requirements = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" +[WARNING] dependency version requirement lacks full precision + --> Cargo.toml:7:7 + | +7 | dep = "1" + | ^^^ + | + = [NOTE] `cargo::imprecise_version_requirements` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -68,6 +75,13 @@ imprecise_version_requirements = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" +[WARNING] dependency version requirement lacks full precision + --> Cargo.toml:7:7 + | +7 | dep = "1.0" + | ^^^^^ + | + = [NOTE] `cargo::imprecise_version_requirements` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -142,6 +156,13 @@ imprecise_version_requirements = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" +[WARNING] dependency version requirement lacks full precision + --> Cargo.toml:7:19 + | +7 | dep = { version = "1" } + | ^^^ + | + = [NOTE] `cargo::imprecise_version_requirements` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -378,6 +399,13 @@ edition = "2021" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" +[WARNING] dependency version requirement lacks full precision + --> Cargo.toml:7:33 + | +7 | bar = { path = "bar", version = "0.1" } + | ^^^^^ + | + = [NOTE] `cargo::imprecise_version_requirements` 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) @@ -461,6 +489,13 @@ imprecise_version_requirements = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" +[WARNING] dependency version requirement lacks full precision + --> Cargo.toml:7:[..] + | +7 | bar = { git = '[ROOTURL]/bar', version = "0.1" } + | [..]^^^^^ + | + = [NOTE] `cargo::imprecise_version_requirements` 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#[..]) @@ -496,6 +531,13 @@ imprecise_version_requirements = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" +[WARNING] dependency version requirement lacks full precision + --> Cargo.toml:7:7 + | +7 | dep = "1" + | ^^^ + | + = [NOTE] `cargo::imprecise_version_requirements` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [CHECKING] foo v0.0.0 ([ROOT]/foo) @@ -531,6 +573,13 @@ imprecise_version_requirements = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" +[WARNING] dependency version requirement lacks full precision + --> Cargo.toml:7:7 + | +7 | dep = "1.0" + | ^^^^^ + | + = [NOTE] `cargo::imprecise_version_requirements` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -569,6 +618,13 @@ imprecise_version_requirements = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" +[WARNING] dependency version requirement lacks full precision + --> Cargo.toml:8:7 + | +8 | dep = "1" + | ^^^ + | + = [NOTE] `cargo::imprecise_version_requirements` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -607,6 +663,13 @@ imprecise_version_requirements = "warn" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" +[WARNING] dependency version requirement lacks full precision + --> Cargo.toml:8:7 + | +8 | dep = "1" + | ^^^ + | + = [NOTE] `cargo::imprecise_version_requirements` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [CHECKING] foo v0.0.0 ([ROOT]/foo) @@ -644,6 +707,20 @@ imprecise_version_requirements = "warn" .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data( str![[r#" +[WARNING] dependency version requirement lacks full precision + --> Cargo.toml:7:7 + | +7 | dep = "1" + | ^^^ + | + = [NOTE] `cargo::imprecise_version_requirements` is set to `warn` in `[lints]` +[WARNING] dependency version requirement lacks full precision + --> Cargo.toml:8:9 + | +8 | regex = "1.0" + | ^^^^^ + | + = [NOTE] `cargo::imprecise_version_requirements` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 2 packages to latest compatible versions [DOWNLOADING] crates ... @@ -700,6 +777,18 @@ workspace = true p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) .with_stderr_data(str![[r#" +[WARNING] dependency version requirement lacks full precision + --> Cargo.toml:7:7 + | +7 | dep = "1" + | ^^^ + | +[NOTE] dependency `dep` was inherited + --> member/Cargo.toml:8:5 + | +8 | dep.workspace = true + | ---------------- + = [NOTE] `cargo::imprecise_version_requirements` is set to `warn` in `[lints]` [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... @@ -736,14 +825,16 @@ imprecise_version_requirements = "deny" p.cargo("check -Zcargo-lints") .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_status(101) .with_stderr_data(str![[r#" -[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 lacks full precision + --> Cargo.toml:7:7 + | +7 | dep = "1" + | ^^^ + | + = [NOTE] `cargo::imprecise_version_requirements` is set to `deny` in `[lints]` +[ERROR] encountered 1 error while running lints "#]]) .run();