diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index 6fd76e42e9e..a0bd9367d4b 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,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.into(), + &path, + &cargo_lints, + &mut error_count, + self.gctx, + )?; } if error_count > 0 { @@ -1332,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 new file mode 100644 index 00000000000..39825e00434 --- /dev/null +++ b/src/cargo/util/lints/implicit_minimum_version_req.rs @@ -0,0 +1,349 @@ +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::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; +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; + +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( + manifest: ManifestFor<'_>, + manifest_path: &Path, + cargo_lints: &TomlToolLints, + error_count: &mut usize, + gctx: &GlobalContext, +) -> CargoResult<()> { + 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) + } + ManifestFor::Workspace(maybe_pkg) => lint_workspace( + maybe_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 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 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], +) -> 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.rs b/src/cargo/util/lints/mod.rs similarity index 95% rename from src/cargo/util/lints.rs rename to src/cargo/util/lints/mod.rs index abb705b53a8..39cb38af525 100644 --- a/src/cargo/util/lints.rs +++ b/src/cargo/util/lints/mod.rs @@ -8,8 +8,49 @@ 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, +]; + +/// Scope at which a lint runs: package-level or workspace-level. +pub enum ManifestFor<'a> { + /// Lint runs for a specific package. + Package(&'a Package), + /// Lint runs for workspace-level config. + Workspace(&'a MaybePackage), +} + +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(), + ), + ManifestFor::Workspace(p) => lint.level(pkg_lints, p.edition(), p.unstable_features()), + } + } +} + +impl<'a> From<&'a Package> for ManifestFor<'a> { + fn from(value: &'a Package) -> ManifestFor<'a> { + ManifestFor::Package(value) + } +} + +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, 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 new file mode 100644 index 00000000000..80044e20f31 --- /dev/null +++ b/tests/testsuite/lints/implicit_minimum_version_req.rs @@ -0,0 +1,1222 @@ +//! 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] 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 ... +[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] 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 ... +[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#" +[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] 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 ... +[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] 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 ... +[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_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#" +[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#" +[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#" +[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#" +[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#" +[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] 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 ... +[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#" +[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#" +[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#" +[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] 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) +[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#" +[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] 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#[..]) +[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] 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) +[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] 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 ... +[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] 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 ... +[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] 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) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn multiple_implicit_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] 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 ... +[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] 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 ... +[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#" +[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 + +"#]]) + .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] 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 + | +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 ... +[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_status(101) + .with_stderr_data(str![[r#" +[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(); +} 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;