fix(resolver): Report unmatched versions, rather than saying no package (#14897)

### What does this PR try to resolve?

Instead of saying no package found when there are hidden `Summary`s,
we'll instead say why the summary was hidden in the cases of
- Yanked packages
- Schema mismatch
- Offline packages?

The schema mismatch covers part of #10623. Whats remaining is when we
can't parse the `Summary` but can parse a subset (name, version, schema
version, optionally rust-version). That will be handled in a follow up.

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

This has a couple of risky areas
- Moving the filtering of `IndexSummary` variants from the Index to the
Registry Source. On inspection, there were other code paths but they
seemed innocuous to not filter, like asking for a hash of a summary
- Switching `PackageRegistry` to preserve the `IndexSummary` variant for
regular sources and overrides
- I did not switch patches to preserve `IndexSummary` as that was more
invasive and the benefits seemed more minor (normally people patch over
registry dependencies and not to them)

### Additional information
This commit is contained in:
Scott Schafer 2024-12-11 16:05:00 +00:00 committed by GitHub
commit ad5b4934b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 194 additions and 81 deletions

View File

@ -143,7 +143,8 @@ pub fn resolve_with_global_context_raw(
for summary in self.list.iter() { for summary in self.list.iter() {
let matched = match kind { let matched = match kind {
QueryKind::Exact => dep.matches(summary), QueryKind::Exact => dep.matches(summary),
QueryKind::Alternatives => true, QueryKind::AlternativeVersions => dep.matches(summary),
QueryKind::AlternativeNames => true,
QueryKind::Normalized => true, QueryKind::Normalized => true,
}; };
if matched { if matched {

View File

@ -674,9 +674,10 @@ impl<'gctx> Registry for PackageRegistry<'gctx> {
let patch = patches.remove(0); let patch = patches.remove(0);
match override_summary { match override_summary {
Some(override_summary) => { Some(override_summary) => {
let override_summary = override_summary.into_summary(); self.warn_bad_override(override_summary.as_summary(), &patch)?;
self.warn_bad_override(&override_summary, &patch)?; let override_summary =
f(IndexSummary::Candidate(self.lock(override_summary))); override_summary.map_summary(|summary| self.lock(summary));
f(override_summary);
} }
None => f(IndexSummary::Candidate(patch)), None => f(IndexSummary::Candidate(patch)),
} }
@ -733,8 +734,8 @@ impl<'gctx> Registry for PackageRegistry<'gctx> {
return; return;
} }
} }
let summary = summary.into_summary(); let summary = summary.map_summary(|summary| lock(locked, all_patches, summary));
f(IndexSummary::Candidate(lock(locked, all_patches, summary))) f(summary)
}; };
return source.query(dep, kind, callback); return source.query(dep, kind, callback);
} }
@ -760,11 +761,11 @@ impl<'gctx> Registry for PackageRegistry<'gctx> {
"found an override with a non-locked list" "found an override with a non-locked list"
))); )));
} }
let override_summary = override_summary.into_summary();
if let Some(to_warn) = to_warn { if let Some(to_warn) = to_warn {
self.warn_bad_override(&override_summary, to_warn.as_summary())?; self.warn_bad_override(override_summary.as_summary(), to_warn.as_summary())?;
} }
f(IndexSummary::Candidate(self.lock(override_summary))); let override_summary = override_summary.map_summary(|summary| self.lock(summary));
f(override_summary);
} }
} }

View File

@ -1,8 +1,10 @@
use std::fmt; use std::fmt;
use std::fmt::Write as _;
use std::task::Poll; use std::task::Poll;
use crate::core::{Dependency, PackageId, Registry, Summary}; use crate::core::{Dependency, PackageId, Registry, Summary};
use crate::sources::source::QueryKind; use crate::sources::source::QueryKind;
use crate::sources::IndexSummary;
use crate::util::edit_distance::edit_distance; use crate::util::edit_distance::edit_distance;
use crate::util::{GlobalContext, OptVersionReq, VersionExt}; use crate::util::{GlobalContext, OptVersionReq, VersionExt};
use anyhow::Error; use anyhow::Error;
@ -301,10 +303,9 @@ pub(super) fn activation_error(
msg msg
} else { } else {
// Maybe the user mistyped the name? Like `dep-thing` when `Dep_Thing` // Maybe something is wrong with the available versions
// was meant. So we try asking the registry for a `fuzzy` search for suggestions. let mut version_candidates = loop {
let candidates = loop { match registry.query_vec(&new_dep, QueryKind::AlternativeVersions) {
match registry.query_vec(&new_dep, QueryKind::Alternatives) {
Poll::Ready(Ok(candidates)) => break candidates, Poll::Ready(Ok(candidates)) => break candidates,
Poll::Ready(Err(e)) => return to_resolve_err(e), Poll::Ready(Err(e)) => return to_resolve_err(e),
Poll::Pending => match registry.block_until_ready() { Poll::Pending => match registry.block_until_ready() {
@ -313,45 +314,103 @@ pub(super) fn activation_error(
}, },
} }
}; };
version_candidates.sort_unstable_by_key(|a| a.as_summary().version().clone());
let mut candidates: Vec<_> = candidates.into_iter().map(|s| s.into_summary()).collect(); // 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.
candidates.sort_unstable_by_key(|a| a.name()); let name_candidates = loop {
candidates.dedup_by(|a, b| a.name() == b.name()); match registry.query_vec(&new_dep, QueryKind::AlternativeNames) {
let mut candidates: Vec<_> = candidates 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),
},
}
};
let mut name_candidates: Vec<_> = name_candidates
.into_iter()
.map(|s| s.into_summary())
.collect();
name_candidates.sort_unstable_by_key(|a| a.name());
name_candidates.dedup_by(|a, b| a.name() == b.name());
let mut name_candidates: Vec<_> = name_candidates
.iter() .iter()
.filter_map(|n| Some((edit_distance(&*new_dep.package_name(), &*n.name(), 3)?, n))) .filter_map(|n| Some((edit_distance(&*new_dep.package_name(), &*n.name(), 3)?, n)))
.collect(); .collect();
candidates.sort_by_key(|o| o.0); name_candidates.sort_by_key(|o| o.0);
let mut msg: String;
if candidates.is_empty() { let mut msg = String::new();
msg = format!("no matching package named `{}` found\n", dep.package_name()); if !version_candidates.is_empty() {
} else { let _ = writeln!(
msg = format!( &mut msg,
"no matching package found\nsearched package name: `{}`\n", "no matching versions for `{}` found",
dep.package_name() dep.package_name()
); );
let mut names = candidates for candidate in version_candidates {
match candidate {
IndexSummary::Candidate(summary) => {
// HACK: If this was a real candidate, we wouldn't hit this case.
// so it must be a patch which get normalized to being a candidate
let _ =
writeln!(&mut msg, " version {} is unavailable", summary.version());
}
IndexSummary::Yanked(summary) => {
let _ = writeln!(&mut msg, " version {} is yanked", summary.version());
}
IndexSummary::Offline(summary) => {
let _ = writeln!(&mut msg, " version {} is not cached", summary.version());
}
IndexSummary::Unsupported(summary, schema_version) => {
if let Some(rust_version) = summary.rust_version() {
// HACK: technically its unsupported and we shouldn't make assumptions
// about the entry but this is limited and for diagnostics purposes
let _ = writeln!(
&mut msg,
" version {} requires cargo {}",
summary.version(),
rust_version
);
} else {
let _ = writeln!(
&mut msg,
" version {} requires a Cargo version that supports index version {}",
summary.version(),
schema_version
);
}
}
}
}
} else if !name_candidates.is_empty() {
let _ = writeln!(&mut msg, "no matching package found",);
let _ = writeln!(&mut msg, "searched package name: `{}`", dep.package_name());
let mut names = name_candidates
.iter() .iter()
.take(3) .take(3)
.map(|c| c.1.name().as_str()) .map(|c| c.1.name().as_str())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if candidates.len() > 3 { if name_candidates.len() > 3 {
names.push("..."); names.push("...");
} }
// Vertically align first suggestion with missing crate name // Vertically align first suggestion with missing crate name
// so a typo jumps out at you. // so a typo jumps out at you.
msg.push_str("perhaps you meant: "); let suggestions = names
msg.push_str(&names.iter().enumerate().fold( .iter()
String::default(), .enumerate()
|acc, (i, el)| match i { .fold(String::default(), |acc, (i, el)| match i {
0 => acc + el, 0 => acc + el,
i if names.len() - 1 == i && candidates.len() <= 3 => acc + " or " + el, i if names.len() - 1 == i && name_candidates.len() <= 3 => acc + " or " + el,
_ => acc + ", " + el, _ => acc + ", " + el,
}, });
)); let _ = writeln!(&mut msg, "perhaps you meant: {suggestions}");
msg.push('\n'); } else {
let _ = writeln!(
&mut msg,
"no matching package named `{}` found",
dep.package_name()
);
} }
let mut location_searched_msg = registry.describe_source(dep.source_id()); let mut location_searched_msg = registry.describe_source(dep.source_id());
@ -359,12 +418,12 @@ pub(super) fn activation_error(
location_searched_msg = format!("{}", dep.source_id()); location_searched_msg = format!("{}", dep.source_id());
} }
msg.push_str(&format!("location searched: {}\n", location_searched_msg)); let _ = writeln!(&mut msg, "location searched: {}", location_searched_msg);
msg.push_str("required by "); let _ = write!(
msg.push_str(&describe_path_in_context( &mut msg,
resolver_ctx, "required by {}",
&parent.package_id(), describe_path_in_context(resolver_ctx, &parent.package_id()),
)); );
msg msg
}; };

View File

@ -108,8 +108,8 @@ impl<'gctx> Source for DirectorySource<'gctx> {
} }
let packages = self.packages.values().map(|p| &p.0); let packages = self.packages.values().map(|p| &p.0);
let matches = packages.filter(|pkg| match kind { let matches = packages.filter(|pkg| match kind {
QueryKind::Exact => dep.matches(pkg.summary()), QueryKind::Exact | QueryKind::AlternativeVersions => dep.matches(pkg.summary()),
QueryKind::Alternatives => true, QueryKind::AlternativeNames => true,
QueryKind::Normalized => dep.matches(pkg.summary()), QueryKind::Normalized => dep.matches(pkg.summary()),
}); });
for summary in matches.map(|pkg| pkg.summary().clone()) { for summary in matches.map(|pkg| pkg.summary().clone()) {

View File

@ -145,8 +145,8 @@ impl<'gctx> Source for PathSource<'gctx> {
self.load()?; self.load()?;
if let Some(s) = self.package.as_ref().map(|p| p.summary()) { if let Some(s) = self.package.as_ref().map(|p| p.summary()) {
let matched = match kind { let matched = match kind {
QueryKind::Exact => dep.matches(s), QueryKind::Exact | QueryKind::AlternativeVersions => dep.matches(s),
QueryKind::Alternatives => true, QueryKind::AlternativeNames => true,
QueryKind::Normalized => dep.matches(s), QueryKind::Normalized => dep.matches(s),
}; };
if matched { if matched {
@ -332,8 +332,8 @@ impl<'gctx> Source for RecursivePathSource<'gctx> {
.map(|p| p.summary()) .map(|p| p.summary())
{ {
let matched = match kind { let matched = match kind {
QueryKind::Exact => dep.matches(s), QueryKind::Exact | QueryKind::AlternativeVersions => dep.matches(s),
QueryKind::Alternatives => true, QueryKind::AlternativeNames => true,
QueryKind::Normalized => dep.matches(s), QueryKind::Normalized => dep.matches(s),
}; };
if matched { if matched {

View File

@ -37,7 +37,7 @@ use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use std::str; use std::str;
use std::task::{ready, Poll}; use std::task::{ready, Poll};
use tracing::{debug, info}; use tracing::info;
mod cache; mod cache;
use self::cache::CacheManager; use self::cache::CacheManager;
@ -370,21 +370,7 @@ impl<'gctx> RegistryIndex<'gctx> {
.filter_map(move |(k, v)| if req.matches(k) { Some(v) } else { None }) .filter_map(move |(k, v)| if req.matches(k) { Some(v) } else { None })
.filter_map(move |maybe| { .filter_map(move |maybe| {
match maybe.parse(raw_data, source_id, bindeps) { match maybe.parse(raw_data, source_id, bindeps) {
Ok(sum @ IndexSummary::Candidate(_) | sum @ IndexSummary::Yanked(_)) => { Ok(sum) => Some(sum),
Some(sum)
}
Ok(IndexSummary::Unsupported(summary, v)) => {
debug!(
"unsupported schema version {} ({} {})",
v,
summary.name(),
summary.version()
);
None
}
Ok(IndexSummary::Offline(_)) => {
unreachable!("We do not check for off-line until later")
}
Err(e) => { Err(e) => {
info!("failed to parse `{}` registry package: {}", name, e); info!("failed to parse `{}` registry package: {}", name, e);
None None

View File

@ -779,7 +779,9 @@ impl<'gctx> Source for RegistrySource<'gctx> {
ready!(self ready!(self
.index .index
.query_inner(dep.package_name(), &req, &mut *self.ops, &mut |s| { .query_inner(dep.package_name(), &req, &mut *self.ops, &mut |s| {
if dep.matches(s.as_summary()) { if matches!(s, IndexSummary::Candidate(_) | IndexSummary::Yanked(_))
&& dep.matches(s.as_summary())
{
// We are looking for a package from a lock file so we do not care about yank // We are looking for a package from a lock file so we do not care about yank
callback(s) callback(s)
} }
@ -797,14 +799,14 @@ impl<'gctx> Source for RegistrySource<'gctx> {
.index .index
.query_inner(dep.package_name(), &req, &mut *self.ops, &mut |s| { .query_inner(dep.package_name(), &req, &mut *self.ops, &mut |s| {
let matched = match kind { let matched = match kind {
QueryKind::Exact => { QueryKind::Exact | QueryKind::AlternativeVersions => {
if req.is_precise() && self.gctx.cli_unstable().unstable_options { if req.is_precise() && self.gctx.cli_unstable().unstable_options {
dep.matches_prerelease(s.as_summary()) dep.matches_prerelease(s.as_summary())
} else { } else {
dep.matches(s.as_summary()) dep.matches(s.as_summary())
} }
} }
QueryKind::Alternatives => true, QueryKind::AlternativeNames => true,
QueryKind::Normalized => true, QueryKind::Normalized => true,
}; };
if !matched { if !matched {
@ -813,14 +815,29 @@ impl<'gctx> Source for RegistrySource<'gctx> {
// Next filter out all yanked packages. Some yanked packages may // Next filter out all yanked packages. Some yanked packages may
// leak through if they're in a whitelist (aka if they were // leak through if they're in a whitelist (aka if they were
// previously in `Cargo.lock` // previously in `Cargo.lock`
if !s.is_yanked() { match s {
callback(s); s @ _ if kind == QueryKind::AlternativeVersions => callback(s),
} else if self.yanked_whitelist.contains(&s.package_id()) { s @ IndexSummary::Candidate(_) => callback(s),
s @ IndexSummary::Yanked(_) => {
if self.yanked_whitelist.contains(&s.package_id()) {
callback(s); callback(s);
} else if req.is_precise() { } else if req.is_precise() {
precise_yanked_in_use = true; precise_yanked_in_use = true;
callback(s); callback(s);
} }
}
IndexSummary::Unsupported(summary, v) => {
tracing::debug!(
"unsupported schema version {} ({} {})",
v,
summary.name(),
summary.version()
);
}
IndexSummary::Offline(summary) => {
tracing::debug!("offline ({} {})", summary.name(), summary.version());
}
}
}))?; }))?;
if precise_yanked_in_use { if precise_yanked_in_use {
let name = dep.package_name(); let name = dep.package_name();
@ -839,7 +856,7 @@ impl<'gctx> Source for RegistrySource<'gctx> {
return Poll::Ready(Ok(())); return Poll::Ready(Ok(()));
} }
let mut any_pending = false; let mut any_pending = false;
if kind == QueryKind::Alternatives || kind == QueryKind::Normalized { if kind == QueryKind::AlternativeNames || kind == QueryKind::Normalized {
// Attempt to handle misspellings by searching for a chain of related // Attempt to handle misspellings by searching for a chain of related
// names to the original name. The resolver will later // names to the original name. The resolver will later
// reject any candidates that have the wrong name, and with this it'll // reject any candidates that have the wrong name, and with this it'll
@ -859,7 +876,7 @@ impl<'gctx> Source for RegistrySource<'gctx> {
.query_inner(name_permutation, &req, &mut *self.ops, &mut |s| { .query_inner(name_permutation, &req, &mut *self.ops, &mut |s| {
if !s.is_yanked() { if !s.is_yanked() {
f(s); f(s);
} else if kind == QueryKind::Alternatives { } else if kind == QueryKind::AlternativeNames {
f(s); f(s);
} }
})? })?

View File

@ -183,9 +183,16 @@ pub enum QueryKind {
/// Each source gets to define what `close` means for it. /// Each source gets to define what `close` means for it.
/// ///
/// Path/Git sources may return all dependencies that are at that URI, /// Path/Git sources may return all dependencies that are at that URI,
/// whereas an `Registry` source may return dependencies that are yanked or invalid.
AlternativeVersions,
/// A query for packages close to the given dependency requirement.
///
/// Each source gets to define what `close` means for it.
///
/// Path/Git sources may return all dependencies that are at that URI,
/// whereas an `Registry` source may return dependencies that have the same /// whereas an `Registry` source may return dependencies that have the same
/// canonicalization. /// canonicalization.
Alternatives, AlternativeNames,
/// Match a dependency in all ways and will normalize the package name. /// Match a dependency in all ways and will normalize the package name.
/// Each source defines what normalizing means. /// Each source defines what normalizing means.
Normalized, Normalized,

View File

@ -924,7 +924,8 @@ fn yanks_in_lockfiles_are_ok_http() {
"#]], "#]],
str![[r#" str![[r#"
[UPDATING] `dummy-registry` index [UPDATING] `dummy-registry` index
[ERROR] no matching package named `bar` found [ERROR] no matching versions for `bar` found
version 0.0.1 is yanked
location searched: `dummy-registry` index (which is replacing registry `crates-io`) location searched: `dummy-registry` index (which is replacing registry `crates-io`)
required by package `foo v0.0.1 ([ROOT]/foo)` required by package `foo v0.0.1 ([ROOT]/foo)`
@ -941,7 +942,8 @@ fn yanks_in_lockfiles_are_ok_git() {
"#]], "#]],
str![[r#" str![[r#"
[UPDATING] `dummy-registry` index [UPDATING] `dummy-registry` index
[ERROR] no matching package named `bar` found [ERROR] no matching versions for `bar` found
version 0.0.1 is yanked
location searched: `dummy-registry` index (which is replacing registry `crates-io`) location searched: `dummy-registry` index (which is replacing registry `crates-io`)
required by package `foo v0.0.1 ([ROOT]/foo)` required by package `foo v0.0.1 ([ROOT]/foo)`
@ -993,7 +995,8 @@ fn yanks_in_lockfiles_are_ok_for_other_update_http() {
"#]], "#]],
str![[r#" str![[r#"
[UPDATING] `dummy-registry` index [UPDATING] `dummy-registry` index
[ERROR] no matching package named `bar` found [ERROR] no matching versions for `bar` found
version 0.0.1 is yanked
location searched: `dummy-registry` index (which is replacing registry `crates-io`) location searched: `dummy-registry` index (which is replacing registry `crates-io`)
required by package `foo v0.0.1 ([ROOT]/foo)` required by package `foo v0.0.1 ([ROOT]/foo)`
@ -1016,7 +1019,8 @@ fn yanks_in_lockfiles_are_ok_for_other_update_git() {
"#]], "#]],
str![[r#" str![[r#"
[UPDATING] `dummy-registry` index [UPDATING] `dummy-registry` index
[ERROR] no matching package named `bar` found [ERROR] no matching versions for `bar` found
version 0.0.1 is yanked
location searched: `dummy-registry` index (which is replacing registry `crates-io`) location searched: `dummy-registry` index (which is replacing registry `crates-io`)
required by package `foo v0.0.1 ([ROOT]/foo)` required by package `foo v0.0.1 ([ROOT]/foo)`
@ -3225,7 +3229,45 @@ fn unknown_index_version_error() {
.with_status(101) .with_status(101)
.with_stderr_data(str![[r#" .with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index [UPDATING] `dummy-registry` index
[ERROR] no matching package named `bar` found [ERROR] no matching versions for `bar` found
version 1.0.1 requires a Cargo version that supports index version 4294967295
location searched: `dummy-registry` index (which is replacing registry `crates-io`)
required by package `foo v0.1.0 ([ROOT]/foo)`
"#]])
.run();
}
#[cargo_test]
fn unknown_index_version_with_msrv_error() {
// If the version field is not understood, it is ignored.
Package::new("bar", "1.0.1")
.schema_version(u32::MAX)
.rust_version("1.2345")
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2015"
[dependencies]
bar = "1.0"
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("generate-lockfile")
.with_status(101)
.with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[ERROR] no matching versions for `bar` found
version 1.0.1 requires cargo 1.2345
location searched: `dummy-registry` index (which is replacing registry `crates-io`) location searched: `dummy-registry` index (which is replacing registry `crates-io`)
required by package `foo v0.1.0 ([ROOT]/foo)` required by package `foo v0.1.0 ([ROOT]/foo)`