Auto merge of #12659 - Eh2406:Prerelease_candidates, r=epage

Prerelease candidates error message

### What does this PR try to resolve?

Error messages reporting on versions that do not match the request incorrectly ignore pre-release versions. This is because the version requirement `"*"` cannot match prerelease versions. #12315

### How should we test and review this PR?

Sorry for the large amount of white space changes, fmt got to fmt. 🤷‍♂️

The process was:
- Revise commit from #12316 (thanks to `@loloicci)` that change the requirement from `"*"` to `Any`
- Move the handling of our special "did you mean to specify a pre-release" code and update tests
- some small re-factoring

### Additional information

The old "did you mean to specify a pre-release" #7191 check only occurred when version requirement does not match any versions and you depended on a package that did not have any non-prerelease versions. Making it rarely useful.
The new one will appear any time your version requirement does not match any versions and the package does have pre-release versions. Which may be too common.
I'm open to suggestions for better heuristic.

It's also not clear that the new message make sense in the case of patched versions.
This commit is contained in:
bors 2023-09-14 01:40:33 +00:00
commit 8d43632ab8
5 changed files with 164 additions and 129 deletions

View File

@ -318,8 +318,8 @@ impl Dependency {
}
/// Sets the version requirement for this dependency.
pub fn set_version_req(&mut self, req: VersionReq) -> &mut Dependency {
Rc::make_mut(&mut self.inner).req = OptVersionReq::Req(req);
pub fn set_version_req(&mut self, req: OptVersionReq) -> &mut Dependency {
Rc::make_mut(&mut self.inner).req = req;
self
}

View File

@ -4,7 +4,7 @@ use std::task::Poll;
use crate::core::{Dependency, PackageId, Registry, Summary};
use crate::sources::source::QueryKind;
use crate::util::edit_distance::edit_distance;
use crate::util::{Config, VersionExt};
use crate::util::{Config, OptVersionReq, VersionExt};
use anyhow::Error;
use super::context::Context;
@ -224,9 +224,8 @@ pub(super) fn activation_error(
// Maybe the user mistyped the ver_req? Like `dep="2"` when `dep="0.2"`
// was meant. So we re-query the registry with `dep="*"` so we can
// list a few versions that were actually found.
let all_req = semver::VersionReq::parse("*").unwrap();
let mut new_dep = dep.clone();
new_dep.set_version_req(all_req);
new_dep.set_version_req(OptVersionReq::Any);
let mut candidates = loop {
match registry.query_vec(&new_dep, QueryKind::Exact) {
@ -241,128 +240,123 @@ pub(super) fn activation_error(
candidates.sort_unstable_by(|a, b| b.version().cmp(a.version()));
let mut msg =
if !candidates.is_empty() {
let versions = {
let mut versions = candidates
.iter()
.take(3)
.map(|cand| cand.version().to_string())
.collect::<Vec<_>>();
if candidates.len() > 3 {
versions.push("...".into());
}
versions.join(", ")
};
let locked_version = dep
.version_req()
.locked_version()
.map(|v| format!(" (locked to {})", v))
.unwrap_or_default();
let mut msg = format!(
"failed to select a version for the requirement `{} = \"{}\"`{}\n\
candidate versions found which didn't match: {}\n\
location searched: {}\n",
dep.package_name(),
dep.version_req(),
locked_version,
versions,
registry.describe_source(dep.source_id()),
);
msg.push_str("required by ");
msg.push_str(&describe_path_in_context(cx, &parent.package_id()));
// If we have a path dependency with a locked version, then this may
// indicate that we updated a sub-package and forgot to run `cargo
// update`. In this case try to print a helpful error!
if dep.source_id().is_path() && dep.version_req().is_locked() {
msg.push_str(
"\nconsider running `cargo update` to update \
a path dependency's locked version",
);
}
if registry.is_replaced(dep.source_id()) {
msg.push_str("\nperhaps a crate was updated and forgotten to be re-vendored?");
}
msg
} else {
// Maybe the user mistyped the name? Like `dep-thing` when `Dep_Thing`
// was meant. So we try asking the registry for a `fuzzy` search for suggestions.
let mut candidates = loop {
match registry.query_vec(&new_dep, QueryKind::Fuzzy) {
Poll::Ready(Ok(candidates)) => break candidates,
Poll::Ready(Err(e)) => return to_resolve_err(e),
Poll::Pending => match registry.block_until_ready() {
Ok(()) => continue,
Err(e) => return to_resolve_err(e),
},
}
};
candidates.sort_unstable_by_key(|a| a.name());
candidates.dedup_by(|a, b| a.name() == b.name());
let mut candidates: Vec<_> = candidates
let mut msg = if !candidates.is_empty() {
let versions = {
let mut versions = candidates
.iter()
.filter_map(|n| Some((edit_distance(&*new_dep.package_name(), &*n.name(), 3)?, n)))
.collect();
candidates.sort_by_key(|o| o.0);
let mut msg: String;
if candidates.is_empty() {
msg = format!("no matching package named `{}` found\n", dep.package_name());
} else {
msg = format!(
"no matching package found\nsearched package name: `{}`\n",
dep.package_name()
);
.take(3)
.map(|cand| cand.version().to_string())
.collect::<Vec<_>>();
// If dependency package name is equal to the name of the candidate here
// it may be a prerelease package which hasn't been specified correctly
if dep.package_name() == candidates[0].1.name()
&& candidates[0].1.package_id().version().is_prerelease()
{
msg.push_str("prerelease package needs to be specified explicitly\n");
msg.push_str(&format!(
"{name} = {{ version = \"{version}\" }}",
name = candidates[0].1.name(),
version = candidates[0].1.package_id().version()
));
} else {
let mut names = candidates
.iter()
.take(3)
.map(|c| c.1.name().as_str())
.collect::<Vec<_>>();
if candidates.len() > 3 {
names.push("...");
}
// Vertically align first suggestion with missing crate name
// so a typo jumps out at you.
msg.push_str("perhaps you meant: ");
msg.push_str(&names.iter().enumerate().fold(
String::default(),
|acc, (i, el)| match i {
0 => acc + el,
i if names.len() - 1 == i && candidates.len() <= 3 => acc + " or " + el,
_ => acc + ", " + el,
},
));
}
msg.push('\n');
if candidates.len() > 3 {
versions.push("...".into());
}
msg.push_str(&format!("location searched: {}\n", dep.source_id()));
msg.push_str("required by ");
msg.push_str(&describe_path_in_context(cx, &parent.package_id()));
msg
versions.join(", ")
};
let locked_version = dep
.version_req()
.locked_version()
.map(|v| format!(" (locked to {})", v))
.unwrap_or_default();
let mut msg = format!(
"failed to select a version for the requirement `{} = \"{}\"`{}\n\
candidate versions found which didn't match: {}\n\
location searched: {}\n",
dep.package_name(),
dep.version_req(),
locked_version,
versions,
registry.describe_source(dep.source_id()),
);
msg.push_str("required by ");
msg.push_str(&describe_path_in_context(cx, &parent.package_id()));
// If we have a pre-release candidate, then that may be what our user is looking for
if let Some(pre) = candidates.iter().find(|c| c.version().is_prerelease()) {
msg.push_str("\nif you are looking for the prerelease package it needs to be specified explicitly");
msg.push_str(&format!(
"\n {} = {{ version = \"{}\" }}",
pre.name(),
pre.version()
));
}
// If we have a path dependency with a locked version, then this may
// indicate that we updated a sub-package and forgot to run `cargo
// update`. In this case try to print a helpful error!
if dep.source_id().is_path() && dep.version_req().is_locked() {
msg.push_str(
"\nconsider running `cargo update` to update \
a path dependency's locked version",
);
}
if registry.is_replaced(dep.source_id()) {
msg.push_str("\nperhaps a crate was updated and forgotten to be re-vendored?");
}
msg
} else {
// Maybe the user mistyped the name? Like `dep-thing` when `Dep_Thing`
// was meant. So we try asking the registry for a `fuzzy` search for suggestions.
let mut candidates = loop {
match registry.query_vec(&new_dep, QueryKind::Fuzzy) {
Poll::Ready(Ok(candidates)) => break candidates,
Poll::Ready(Err(e)) => return to_resolve_err(e),
Poll::Pending => match registry.block_until_ready() {
Ok(()) => continue,
Err(e) => return to_resolve_err(e),
},
}
};
candidates.sort_unstable_by_key(|a| a.name());
candidates.dedup_by(|a, b| a.name() == b.name());
let mut candidates: Vec<_> = candidates
.iter()
.filter_map(|n| Some((edit_distance(&*new_dep.package_name(), &*n.name(), 3)?, n)))
.collect();
candidates.sort_by_key(|o| o.0);
let mut msg: String;
if candidates.is_empty() {
msg = format!("no matching package named `{}` found\n", dep.package_name());
} else {
msg = format!(
"no matching package found\nsearched package name: `{}`\n",
dep.package_name()
);
let mut names = candidates
.iter()
.take(3)
.map(|c| c.1.name().as_str())
.collect::<Vec<_>>();
if candidates.len() > 3 {
names.push("...");
}
// Vertically align first suggestion with missing crate name
// so a typo jumps out at you.
msg.push_str("perhaps you meant: ");
msg.push_str(&names.iter().enumerate().fold(
String::default(),
|acc, (i, el)| match i {
0 => acc + el,
i if names.len() - 1 == i && candidates.len() <= 3 => acc + " or " + el,
_ => acc + ", " + el,
},
));
msg.push('\n');
}
msg.push_str(&format!("location searched: {}\n", dep.source_id()));
msg.push_str("required by ");
msg.push_str(&describe_path_in_context(cx, &parent.package_id()));
msg
};
if let Some(config) = config {
if config.offline() {
msg.push_str(

View File

@ -10,7 +10,6 @@ use cargo_platform::Platform;
use cargo_util::paths;
use itertools::Itertools;
use lazycell::LazyCell;
use semver::{self, VersionReq};
use serde::de::{self, IntoDeserializer as _, Unexpected};
use serde::ser;
use serde::{Deserialize, Serialize};
@ -30,8 +29,8 @@ use crate::sources::{CRATES_IO_INDEX, CRATES_IO_REGISTRY};
use crate::util::errors::{CargoResult, ManifestError};
use crate::util::interning::InternedString;
use crate::util::{
self, config::ConfigRelativePath, validate_package_name, Config, IntoUrl, PartialVersion,
VersionReqExt,
self, config::ConfigRelativePath, validate_package_name, Config, IntoUrl, OptVersionReq,
PartialVersion,
};
pub mod embedded;
@ -2656,7 +2655,7 @@ impl TomlManifest {
replacement.unused_keys(),
&mut cx.warnings,
);
dep.set_version_req(VersionReq::exact(version))
dep.set_version_req(OptVersionReq::exact(version))
.lock_version(version);
replace.push((spec, dep));
}

View File

@ -2658,3 +2658,45 @@ failed to select a version for `qux` which could resolve this conflict"#,
)
.run();
}
#[cargo_test]
fn mismatched_version_with_prerelease() {
Package::new("prerelease-deps", "0.0.1").publish();
// A patch to a location that has an prerelease version
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
prerelease-deps = "0.1.0"
[patch.crates-io]
prerelease-deps = { path = "./prerelease-deps" }
"#,
)
.file("src/lib.rs", "")
.file(
"prerelease-deps/Cargo.toml",
&basic_manifest("prerelease-deps", "0.1.1-pre1"),
)
.file("prerelease-deps/src/lib.rs", "")
.build();
p.cargo("generate-lockfile")
.with_status(101)
.with_stderr(
r#"[UPDATING] `dummy-registry` index
[ERROR] failed to select a version for the requirement `prerelease-deps = "^0.1.0"`
candidate versions found which didn't match: 0.1.1-pre1, 0.0.1
location searched: `dummy-registry` index (which is replacing registry `crates-io`)
required by package `foo v0.1.0 [..]`
if you are looking for the prerelease package it needs to be specified explicitly
prerelease-deps = { version = "0.1.1-pre1" }
perhaps a crate was updated and forgotten to be re-vendored?"#,
)
.run();
}

View File

@ -1756,12 +1756,12 @@ fn use_semver_package_incorrectly() {
.with_status(101)
.with_stderr(
"\
error: no matching package found
searched package name: `a`
prerelease package needs to be specified explicitly
a = { version = \"0.1.1-alpha.0\" }
error: failed to select a version for the requirement `a = \"^0.1\"`
candidate versions found which didn't match: 0.1.1-alpha.0
location searched: [..]
required by package `b v0.1.0 ([..])`
if you are looking for the prerelease package it needs to be specified explicitly
a = { version = \"0.1.1-alpha.0\" }
",
)
.run();