use deps_core::Deprecation;
use deps_core::PackageName;
use deps_core::lsp_helpers::DependencyOutcomes;
use std::collections::{HashMap, HashSet};
pub fn merge_deprecations_after_fetch(
outcomes: &mut DependencyOutcomes,
fetched_names: &[PackageName],
mut fetched_deprecations: HashMap<PackageName, Deprecation>,
formatter: &dyn deps_core::lsp_helpers::EcosystemFormatter,
) {
let mut per_normalized: HashMap<String, Option<Deprecation>> = HashMap::new();
for name in fetched_names {
let normalized = formatter.normalize_package_name(name);
let found = fetched_deprecations.remove(name);
let entry = per_normalized.entry(normalized).or_insert(None);
if entry.is_none() {
*entry = found;
}
}
for (normalized, deprecation) in per_normalized {
match deprecation {
Some(deprecation) => {
outcomes.set_deprecation(normalized, deprecation);
}
None => {
outcomes.clear_deprecation(&normalized);
}
}
}
}
pub fn merge_no_comparable_versions_after_fetch(
outcomes: &mut DependencyOutcomes,
attempted_names: &[PackageName],
mut fetched_no_comparable_versions: HashSet<PackageName>,
formatter: &dyn deps_core::lsp_helpers::EcosystemFormatter,
) {
let mut per_normalized: HashMap<String, bool> = HashMap::new();
for name in attempted_names {
let normalized = formatter.normalize_package_name(name);
let found = fetched_no_comparable_versions.remove(name);
let entry = per_normalized.entry(normalized).or_insert(false);
*entry = *entry || found;
}
for (normalized, found) in per_normalized {
if found {
outcomes.set_no_comparable_versions(normalized);
} else {
outcomes.clear_no_comparable_versions(&normalized);
}
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "cargo")]
mod cargo_tests {
use super::super::*;
use crate::setup::CargoFormatter;
#[test]
fn test_merge_deprecations_after_fetch_retains_finding_for_name_not_refetched() {
let formatter = CargoFormatter;
let mut outcomes = DependencyOutcomes::new();
outcomes.set_deprecation(
"vendor/a".to_string(),
Deprecation {
reason: None,
replacement: Some("vendor/a2".to_string()),
},
);
let mut fetched = HashMap::new();
fetched.insert(
PackageName::new("vendor/b"),
Deprecation {
reason: Some("abandoned".to_string()),
replacement: None,
},
);
merge_deprecations_after_fetch(
&mut outcomes,
&[PackageName::new("vendor/b")],
fetched,
&formatter,
);
assert_eq!(
outcomes.deprecation("vendor/a"),
Some(&Deprecation {
reason: None,
replacement: Some("vendor/a2".to_string()),
}),
"a finding for a name not in this round's fetch must survive untouched"
);
assert_eq!(
outcomes.deprecation("vendor/b"),
Some(&Deprecation {
reason: Some("abandoned".to_string()),
replacement: None,
})
);
}
#[test]
fn test_merge_deprecations_after_fetch_clears_finding_when_refetched_clean() {
let formatter = CargoFormatter;
let mut outcomes = DependencyOutcomes::new();
outcomes.set_deprecation(
"vendor/a".to_string(),
Deprecation {
reason: None,
replacement: None,
},
);
merge_deprecations_after_fetch(
&mut outcomes,
&[PackageName::new("vendor/a")],
HashMap::new(),
&formatter,
);
assert!(
outcomes.deprecation("vendor/a").is_none(),
"a name that was fetched and produced no finding must be cleared"
);
}
#[test]
fn test_merge_no_comparable_versions_after_fetch_sets_finding_for_newly_flagged_name() {
let formatter = CargoFormatter;
let mut outcomes = DependencyOutcomes::new();
let mut fetched = HashSet::new();
fetched.insert(PackageName::new("vendor/b"));
merge_no_comparable_versions_after_fetch(
&mut outcomes,
&[PackageName::new("vendor/b")],
fetched,
&formatter,
);
assert!(
outcomes.no_comparable_versions("vendor/b"),
"a package whose fetch was attempted and found zero comparable versions \
this round must be recorded"
);
assert!(
!outcomes.no_comparable_versions("vendor/a"),
"a package never attempted this round must not be flagged"
);
}
#[test]
fn test_merge_no_comparable_versions_after_fetch_clears_finding_when_refetched_with_versions()
{
let formatter = CargoFormatter;
let mut outcomes = DependencyOutcomes::new();
outcomes.set_no_comparable_versions("vendor/a".to_string());
merge_no_comparable_versions_after_fetch(
&mut outcomes,
&[PackageName::new("vendor/a")],
HashSet::new(),
&formatter,
);
assert!(
!outcomes.no_comparable_versions("vendor/a"),
"a package that was attempted and this time resolved a real version must \
have its stale marker cleared, or R5e would keep suppressing Unknown \
package diagnostics for a name that could now legitimately need one"
);
}
#[test]
fn test_merge_no_comparable_versions_after_fetch_retains_finding_for_name_not_attempted() {
let formatter = CargoFormatter;
let mut outcomes = DependencyOutcomes::new();
outcomes.set_no_comparable_versions("vendor/a".to_string());
merge_no_comparable_versions_after_fetch(
&mut outcomes,
&[PackageName::new("vendor/b")],
HashSet::new(),
&formatter,
);
assert!(
outcomes.no_comparable_versions("vendor/a"),
"a finding for a name not attempted this round must survive untouched"
);
}
}
#[cfg(feature = "composer")]
mod composer_tests {
use super::super::*;
use crate::setup::ComposerFormatter;
#[test]
fn test_merge_deprecations_after_fetch_is_order_independent_across_normalization_collision()
{
let formatter = ComposerFormatter;
for names in [
[
PackageName::new("vendor/package"),
PackageName::new("Vendor/Package"),
],
[
PackageName::new("Vendor/Package"),
PackageName::new("vendor/package"),
],
] {
let mut outcomes = DependencyOutcomes::new();
let mut fetched = HashMap::new();
fetched.insert(
PackageName::new("Vendor/Package"),
Deprecation {
reason: None,
replacement: Some("vendor/other".to_string()),
},
);
merge_deprecations_after_fetch(&mut outcomes, &names, fetched, &formatter);
assert_eq!(
outcomes.deprecation("vendor/package"),
Some(&Deprecation {
reason: None,
replacement: Some("vendor/other".to_string()),
}),
"a finding under either raw name sharing a normalized key must survive, \
regardless of fetch order: {names:?}"
);
}
}
}
}