use std::collections::HashMap;
use tower_lsp_server::ls_types::{
CodeDescription, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, Location,
NumberOrString, Position, Range, Uri,
};
use crate::osv::{ScanOutcome, diagnostic_severity_for};
use crate::{
ConcreteVersion, Dependency, Deprecation, FetchFailure, ParseResult, PublishTime,
RemovalStatus, VersionReq, format_relative_age, is_within_cooldown,
};
use super::{
EcosystemFormatter, PackageVersions, RequirementMatcher, RequirementStatus, VersionData,
};
pub const UNSATISFIABLE_DIAGNOSTIC_CODE: &str = "unsatisfiable-requirement";
const MAX_BLOCKED_REGISTRY_MESSAGE_VALUE_CHARS: usize = 128;
pub fn truncate_for_diagnostic(value: &str, max_chars: usize) -> std::borrow::Cow<'_, str> {
if value.chars().count() <= max_chars {
return std::borrow::Cow::Borrowed(value);
}
let mut truncated: String = value.chars().take(max_chars).collect();
truncated.push('…');
std::borrow::Cow::Owned(truncated)
}
pub const DEPRECATED_DIAGNOSTIC_CODE: &str = "deprecated-package";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DiagnosticSeverities {
pub outdated: DiagnosticSeverity,
pub unknown: DiagnosticSeverity,
pub yanked: DiagnosticSeverity,
pub unsatisfiable: DiagnosticSeverity,
pub deprecated: DiagnosticSeverity,
pub mutable_ref_pin: DiagnosticSeverity,
pub mutable_ref_pin_enabled: bool,
}
impl Default for DiagnosticSeverities {
fn default() -> Self {
Self {
outdated: DiagnosticSeverity::HINT,
unknown: DiagnosticSeverity::WARNING,
yanked: DiagnosticSeverity::WARNING,
unsatisfiable: DiagnosticSeverity::WARNING,
deprecated: DiagnosticSeverity::WARNING,
mutable_ref_pin: DiagnosticSeverity::HINT,
mutable_ref_pin_enabled: true,
}
}
}
pub fn compile_requirement_unless<M>(
requirement: &str,
is_undecidable: impl FnOnce(&str) -> bool,
matcher: impl FnOnce(String) -> M,
) -> Option<Box<dyn RequirementMatcher>>
where
M: RequirementMatcher + 'static,
{
if is_undecidable(requirement) {
return None;
}
Some(Box::new(matcher(requirement.to_string())))
}
const MAX_REQUIREMENT_LEN: usize = 256;
pub fn requirement_is_unsatisfiable(
formatter: &dyn EcosystemFormatter,
requirement: &VersionReq,
available: &[ConcreteVersion],
) -> bool {
if available.is_empty() || requirement.as_str().trim().is_empty() {
return false;
}
if requirement.as_str().len() > MAX_REQUIREMENT_LEN {
return false;
}
if formatter.requirement_is_unresolved(requirement) {
return false;
}
if formatter.requirement_is_undecidable_given_available(requirement, available) {
return false;
}
let Some(matcher) = formatter.compile_requirement(requirement) else {
return false;
};
let mut saw_decided_false = false;
for candidate in available {
match matcher.matches(candidate) {
Some(true) => return false,
Some(false) => saw_decided_false = true,
None => {}
}
}
saw_decided_false
}
fn semver_prerelease_base(version: &str) -> Option<&str> {
let core = version.split('+').next().unwrap_or(version);
core.find('-').map(|dash| &core[..dash])
}
fn requirement_names_prerelease(requirement: &str) -> bool {
let bytes = requirement.as_bytes();
bytes.iter().enumerate().any(|(i, &b)| {
b == b'-'
&& i > 0
&& i + 1 < bytes.len()
&& !bytes[i - 1].is_ascii_whitespace()
&& !bytes[i + 1].is_ascii_whitespace()
})
}
fn matching_prerelease_would_satisfy(
formatter: &dyn EcosystemFormatter,
requirement: &VersionReq,
available: &[ConcreteVersion],
yanked: &[(ConcreteVersion, RemovalStatus)],
) -> Option<String> {
if !formatter.strict_semver_prerelease_exclusion() {
return None;
}
if requirement_names_prerelease(requirement.as_str()) {
return None;
}
let matcher = formatter.compile_requirement(requirement)?;
available.iter().find_map(|candidate| {
let base = semver_prerelease_base(candidate.as_str())?;
(!yanked.iter().any(|(y, _)| y == candidate)
&& matcher.matches(&ConcreteVersion::new(base)) == Some(true))
.then(|| candidate.to_string())
})
}
fn status_for_version(
yanked: &[(ConcreteVersion, RemovalStatus)],
candidate: &ConcreteVersion,
) -> Option<RemovalStatus> {
let mut found: Option<RemovalStatus> = None;
for (y, status) in yanked {
if y == candidate {
if *status == RemovalStatus::Yanked {
return Some(RemovalStatus::Yanked);
}
found = Some(*status);
}
}
found
}
fn requirement_matches_only_yanked(
formatter: &dyn EcosystemFormatter,
requirement: &VersionReq,
available: &[ConcreteVersion],
yanked: &[(ConcreteVersion, RemovalStatus)],
) -> Option<RemovalStatus> {
if available.is_empty() || yanked.is_empty() || requirement.as_str().trim().is_empty() {
return None;
}
if requirement.as_str().len() > MAX_REQUIREMENT_LEN {
return None;
}
if formatter.requirement_is_unresolved(requirement) {
return None;
}
let matcher = formatter.compile_requirement(requirement)?;
let mut saw_match = false;
let mut saw_undecided = false;
let mut aggregate: Option<RemovalStatus> = None;
for candidate in available {
match matcher.matches(candidate) {
Some(true) => {
saw_match = true;
let status = status_for_version(yanked, candidate)?;
if aggregate != Some(RemovalStatus::Yanked) {
aggregate = Some(status);
}
}
Some(false) => {}
None => saw_undecided = true,
}
}
if saw_match && !saw_undecided {
aggregate
} else {
None
}
}
pub fn generate_diagnostics_from_cache(
parse_result: &dyn ParseResult,
versions: VersionData<'_>,
formatter: &dyn EcosystemFormatter,
uri: &Uri,
freshness: crate::freshness::FreshnessSettings,
severities: DiagnosticSeverities,
now: PublishTime,
) -> Vec<Diagnostic> {
let deps = parse_result.dependencies();
let mut diagnostics = Vec::with_capacity(deps.len());
let mut fetch_failed: Vec<FetchFailureEntry> = Vec::new();
offline_notice(&mut diagnostics, versions, &deps);
blocked_registry_diagnostics(&mut diagnostics, parse_result);
let vuln_keys = versions.ecosystem.map(|ecosystem| {
crate::osv::vulnerability_keys(parse_result, versions.resolved, formatter, ecosystem)
});
for dep in deps {
let normalized_name = formatter.normalize_package_name(dep.name());
let ctx = RuleContext {
dep,
normalized_name: &normalized_name,
versions,
formatter,
severities,
freshness,
now,
};
apply_vulnerability_rule(&mut diagnostics, &ctx, vuln_keys.as_ref());
let deprecation_found = apply_deprecation_rule(&mut diagnostics, &ctx);
let in_use_yanked_emitted =
apply_in_use_yanked_rule(&mut diagnostics, &ctx, deprecation_found);
let Some(package_versions) = ctx.cached_versions() else {
apply_unknown_package_rule(&mut diagnostics, &mut fetch_failed, &ctx);
continue;
};
let Some(version_range) = dep.version_range() else {
continue;
};
let resolved = ResolvedData {
package_versions,
version_range,
};
if apply_unsatisfiable_rule(&mut diagnostics, &ctx, &resolved) == RuleFlow::Stop {
continue;
}
if apply_yanked_only_rule(
&mut diagnostics,
&ctx,
&resolved,
YankedOnlyPrior {
deprecation_found,
in_use_yanked_emitted,
},
) == RuleFlow::Stop
{
continue;
}
apply_outdated_rule(&mut diagnostics, &ctx, &resolved);
}
push_collapsed_fetch_failures(&mut diagnostics, fetch_failed, uri);
diagnostics
}
struct RuleContext<'a> {
dep: &'a dyn Dependency,
normalized_name: &'a str,
versions: VersionData<'a>,
formatter: &'a dyn EcosystemFormatter,
severities: DiagnosticSeverities,
freshness: crate::freshness::FreshnessSettings,
now: PublishTime,
}
impl<'a> RuleContext<'a> {
fn cached_versions(&self) -> Option<&'a PackageVersions> {
self.versions
.cached
.get(self.normalized_name)
.or_else(|| self.versions.cached.get(self.dep.name()))
}
}
struct ResolvedData<'a> {
package_versions: &'a PackageVersions,
version_range: Range,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RuleFlow {
Continue,
Stop,
}
struct FetchFailureEntry {
name: String,
diagnostic: Diagnostic,
failure: Option<FetchFailure>,
}
struct YankedOnlyPrior {
deprecation_found: bool,
in_use_yanked_emitted: bool,
}
fn offline_notice(
diagnostics: &mut Vec<Diagnostic>,
versions: VersionData<'_>,
deps: &[&dyn Dependency],
) {
if versions.offline && !deps.is_empty() {
diagnostics.push(Diagnostic {
range: Range {
start: Position::new(0, 0),
end: Position::new(0, 0),
},
severity: Some(DiagnosticSeverity::INFORMATION),
message: "deps-lsp is offline (network.offline): dependency and vulnerability \
data reflects only what was already cached, not the current registry state"
.to_string(),
source: Some("deps-lsp".into()),
..Default::default()
});
}
}
fn blocked_registry_diagnostics(diagnostics: &mut Vec<Diagnostic>, parse_result: &dyn ParseResult) {
for (range, class, raw_value) in parse_result.blocked_registries() {
diagnostics.push(Diagnostic {
range,
severity: Some(DiagnosticSeverity::INFORMATION),
message: format!(
"registry index \"{}\" blocked by registries.workspace_registries policy \
(host class: {class})",
truncate_for_diagnostic(&raw_value, MAX_BLOCKED_REGISTRY_MESSAGE_VALUE_CHARS)
),
source: Some("deps-lsp".into()),
..Default::default()
});
}
}
fn apply_vulnerability_rule(
diagnostics: &mut Vec<Diagnostic>,
ctx: &RuleContext<'_>,
vuln_keys: Option<&HashMap<Range, String>>,
) {
if let Some(vulnerabilities) = ctx.versions.vulnerabilities
&& let Some(ScanOutcome::Vulnerable(dv)) = vuln_keys
.and_then(|keys| keys.get(&ctx.dep.name_range()))
.and_then(|key| vulnerabilities.get(key))
.or_else(|| vulnerabilities.get(ctx.normalized_name))
.or_else(|| vulnerabilities.get(ctx.dep.name().as_str()))
{
push_vulnerability_diagnostics(diagnostics, ctx.dep, dv);
}
}
fn apply_deprecation_rule(diagnostics: &mut Vec<Diagnostic>, ctx: &RuleContext<'_>) -> bool {
let deprecation = ctx
.formatter
.can_resolve_source(&ctx.dep.source())
.then(|| {
ctx.versions
.outcomes
.and_then(|o| o.deprecation(ctx.normalized_name))
})
.flatten();
if let Some(dep_info) = deprecation {
push_deprecation_diagnostic(
diagnostics,
ctx.dep,
ctx.formatter,
dep_info,
ctx.severities,
);
}
deprecation.is_some()
}
fn apply_in_use_yanked_rule(
diagnostics: &mut Vec<Diagnostic>,
ctx: &RuleContext<'_>,
deprecation_found: bool,
) -> bool {
let deprecation_suppresses_yanked = deprecation_found
&& ctx
.versions
.outcomes
.and_then(|o| o.yanked(ctx.normalized_name))
.is_some_and(|(_, status)| *status != RemovalStatus::Yanked);
if deprecation_suppresses_yanked {
false
} else if let Some((yanked_version, _status)) = ctx
.versions
.outcomes
.and_then(|o| o.yanked(ctx.normalized_name))
&& ctx.versions.ecosystem.is_none_or(|ecosystem| {
super::in_use_version(
ctx.dep,
ctx.normalized_name,
ctx.versions.resolved,
ctx.formatter,
ecosystem,
)
.as_deref()
== Some(yanked_version.as_str())
})
{
diagnostics.push(Diagnostic {
range: ctx
.dep
.version_range()
.unwrap_or_else(|| ctx.dep.name_range()),
severity: Some(ctx.severities.yanked),
message: format!("{} ({})", ctx.formatter.yanked_message(), yanked_version),
source: Some("deps-lsp".into()),
..Default::default()
});
true
} else {
false
}
}
fn apply_unknown_package_rule(
diagnostics: &mut Vec<Diagnostic>,
fetch_failed: &mut Vec<FetchFailureEntry>,
ctx: &RuleContext<'_>,
) {
let dep = ctx.dep;
let in_lockfile = ctx.versions.resolved.contains_key(ctx.normalized_name)
|| ctx.versions.resolved.contains_key(dep.name());
if in_lockfile {
return;
}
let fetch_failure: Option<&FetchFailure> = ctx
.formatter
.can_resolve_source(&dep.source())
.then(|| {
ctx.versions
.outcomes
.and_then(|o| o.fetch_failure(ctx.normalized_name))
})
.flatten();
match ctx.formatter.validate_package_name(dep.name().as_str()) {
Err(reason) => {
diagnostics.push(Diagnostic {
range: dep.name_range(),
severity: Some(ctx.severities.unknown),
message: format!("Invalid package name '{}': {reason}", dep.name()),
source: Some("deps-lsp".into()),
..Default::default()
});
}
Ok(()) if fetch_failure.is_some() && ctx.versions.offline => {}
Ok(()) if fetch_failure.is_some() => {
let message = match fetch_failure {
Some(FetchFailure::Actionable(hint)) => {
format!("Registry lookup failed for '{}': {hint}", dep.name())
}
Some(FetchFailure::Transient | FetchFailure::NotAttempted) | None => {
format!(
"Registry lookup failed for '{}'; package status could not be determined",
dep.name()
)
}
};
fetch_failed.push(FetchFailureEntry {
name: dep.name().to_string(),
diagnostic: Diagnostic {
range: dep.name_range(),
severity: Some(ctx.severities.unknown),
message,
source: Some("deps-lsp".into()),
..Default::default()
},
failure: fetch_failure.cloned(),
});
}
Ok(()) if ctx.formatter.can_resolve_source(&dep.source()) => {
diagnostics.push(Diagnostic {
range: dep.name_range(),
severity: Some(ctx.severities.unknown),
message: format!("Unknown package '{}'", dep.name()),
source: Some("deps-lsp".into()),
..Default::default()
});
}
Ok(()) => {}
}
}
fn apply_unsatisfiable_rule(
diagnostics: &mut Vec<Diagnostic>,
ctx: &RuleContext<'_>,
resolved: &ResolvedData<'_>,
) -> RuleFlow {
let dep = ctx.dep;
let package_versions = resolved.package_versions;
let latest = &package_versions.latest;
let unsatisfiable = ctx.formatter.can_resolve_source(&dep.source())
&& dep.version_requirement().is_some_and(|version_req| {
requirement_is_unsatisfiable(ctx.formatter, version_req, &package_versions.available)
});
if !unsatisfiable {
return RuleFlow::Continue;
}
let req_str = dep.version_requirement().map_or("", |r| r.as_str());
let mut message =
format!("No published version satisfies requirement '{req_str}'; latest is {latest}");
if let Some(prerelease) = dep.version_requirement().and_then(|version_req| {
matching_prerelease_would_satisfy(
ctx.formatter,
version_req,
&package_versions.available,
&package_versions.yanked,
)
}) {
use std::fmt::Write as _;
let _ = write!(
message,
" (a pre-release, {prerelease}, is excluded by SemVer's default \
pre-release-matching rules; require it explicitly to use it)"
);
}
diagnostics.push(Diagnostic {
range: resolved.version_range,
severity: Some(ctx.severities.unsatisfiable),
message,
source: Some("deps-lsp".into()),
code: Some(NumberOrString::String(UNSATISFIABLE_DIAGNOSTIC_CODE.into())),
..Default::default()
});
RuleFlow::Stop
}
fn apply_yanked_only_rule(
diagnostics: &mut Vec<Diagnostic>,
ctx: &RuleContext<'_>,
resolved: &ResolvedData<'_>,
prior: YankedOnlyPrior,
) -> RuleFlow {
let YankedOnlyPrior {
deprecation_found,
in_use_yanked_emitted,
} = prior;
let dep = ctx.dep;
let package_versions = resolved.package_versions;
let latest = &package_versions.latest;
let yanked_only_status = (!in_use_yanked_emitted
&& ctx.formatter.can_resolve_source(&dep.source()))
.then(|| dep.version_requirement())
.flatten()
.filter(|version_req| ctx.formatter.yanked_diagnostic_applies_to(dep, version_req))
.and_then(|version_req| {
requirement_matches_only_yanked(
ctx.formatter,
version_req,
&package_versions.available,
&package_versions.yanked,
)
});
let yanked_only = yanked_only_status
.is_some_and(|status| status != RemovalStatus::AdvisoryDeprecated || !deprecation_found);
if !yanked_only {
return RuleFlow::Continue;
}
diagnostics.push(Diagnostic {
range: resolved.version_range,
severity: Some(ctx.severities.yanked),
message: format!("{}; latest is {latest}", ctx.formatter.yanked_message()),
source: Some("deps-lsp".into()),
..Default::default()
});
RuleFlow::Stop
}
fn apply_outdated_rule(
diagnostics: &mut Vec<Diagnostic>,
ctx: &RuleContext<'_>,
resolved: &ResolvedData<'_>,
) {
let dep = ctx.dep;
let package_versions = resolved.package_versions;
let latest = &package_versions.latest;
let status = match dep.version_requirement() {
Some(version_req) if ctx.formatter.can_resolve_source(&dep.source()) => {
ctx.formatter.requirement_status(version_req, latest)
}
_ => RequirementStatus::Unresolved,
};
if status != RequirementStatus::Outdated {
return;
}
let published_at = ctx
.freshness
.enabled
.then_some(package_versions.published_at)
.flatten();
let message = match published_at {
Some(published_at)
if is_within_cooldown(
published_at.age_secs_from(ctx.now),
ctx.freshness.cooldown_secs,
) =>
{
format!(
"Newer version available: {latest} (published {} — still within the release cooldown window)",
format_relative_age(published_at.age_secs_from(ctx.now))
)
}
_ => format!("Newer version available: {latest}"),
};
diagnostics.push(Diagnostic {
range: resolved.version_range,
severity: Some(ctx.severities.outdated),
message,
source: Some("deps-lsp".into()),
..Default::default()
});
}
fn push_collapsed_fetch_failures(
diagnostics: &mut Vec<Diagnostic>,
fetch_failed: Vec<FetchFailureEntry>,
uri: &Uri,
) {
match fetch_failed.len() {
0 => {}
1 => diagnostics.extend(fetch_failed.into_iter().map(|entry| entry.diagnostic)),
n => {
let first = &fetch_failed[0].diagnostic;
let range = first.range;
let severity = first.severity;
let shared_hint = fetch_failed.iter().find_map(|entry| match &entry.failure {
Some(FetchFailure::Actionable(hint)) => Some(hint.clone()),
_ => None,
});
let message = match shared_hint {
Some(hint) => format!("Registry lookup failed for {n} packages: {hint}"),
None => format!(
"Registry lookup failed for {n} packages; package status could not be determined"
),
};
let related_information = fetch_failed[1..]
.iter()
.map(|entry| DiagnosticRelatedInformation {
location: Location {
uri: uri.clone(),
range: entry.diagnostic.range,
},
message: format!("'{}' also failed", entry.name),
})
.collect();
diagnostics.push(Diagnostic {
range,
severity,
message,
source: Some("deps-lsp".into()),
related_information: Some(related_information),
..Default::default()
});
}
}
}
fn push_deprecation_diagnostic(
diagnostics: &mut Vec<Diagnostic>,
dep: &dyn Dependency,
formatter: &dyn EcosystemFormatter,
deprecation: &Deprecation,
severities: DiagnosticSeverities,
) {
use std::fmt::Write as _;
let range = dep.version_range().unwrap_or_else(|| dep.name_range());
let mut message = formatter.deprecated_message().to_string();
if let Some(reason) = deprecation.reason.as_deref().filter(|r| !r.is_empty()) {
let _ = write!(message, ": {reason}");
}
if let Some(replacement) = deprecation.replacement.as_deref().filter(|r| !r.is_empty()) {
let _ = write!(message, " (replacement: {replacement})");
}
diagnostics.push(Diagnostic {
range,
severity: Some(severities.deprecated),
message,
source: Some("deps-lsp".into()),
code: Some(NumberOrString::String(DEPRECATED_DIAGNOSTIC_CODE.into())),
..Default::default()
});
}
fn push_vulnerability_diagnostics(
diagnostics: &mut Vec<Diagnostic>,
dep: &dyn Dependency,
dv: &crate::osv::DependencyVulnerabilities,
) {
let range = dep.version_range().unwrap_or_else(|| dep.name_range());
for advisory in dv.advisories.items() {
let code_description = advisory
.url
.parse::<Uri>()
.ok()
.map(|href| CodeDescription { href });
diagnostics.push(Diagnostic {
range,
severity: Some(diagnostic_severity_for(advisory.severity)),
message: format!(
"{}: {}",
advisory.id,
advisory
.summary
.as_deref()
.unwrap_or("(no summary provided)")
),
code: Some(NumberOrString::String(advisory.id.clone())),
code_description,
source: Some("deps-lsp".into()),
..Default::default()
});
}
let remaining = dv.advisories.remaining();
if remaining > 0 {
diagnostics.push(Diagnostic {
range,
severity: Some(DiagnosticSeverity::INFORMATION),
message: format!("+{remaining} more advisories"),
source: Some("deps-lsp".into()),
..Default::default()
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lsp_helpers::test_support::*;
use crate::lsp_helpers::*;
use crate::{PackageName, VersionReq};
use std::collections::HashMap;
use std::sync::Arc;
#[test]
fn test_generate_diagnostics_from_cache_unknown_package() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "unknown-pkg".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 11)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
assert!(diagnostics[0].message.contains("Unknown package"));
assert!(diagnostics[0].message.contains("unknown-pkg"));
}
#[test]
fn test_generate_diagnostics_from_cache_emits_blocked_registry_diagnostic() {
use crate::net_policy::HostClass;
use tower_lsp_server::ls_types::{Position, Range};
struct BlockedRegistryParseResult {
deps: Vec<MockDep>,
uri: Uri,
blocked: Vec<(Range, HostClass, String)>,
}
impl ParseResult for BlockedRegistryParseResult {
fn dependencies(&self) -> Vec<&dyn Dependency> {
self.deps.iter().map(|d| d as &dyn Dependency).collect()
}
fn workspace_root(&self) -> Option<&std::path::Path> {
None
}
fn uri(&self) -> &Uri {
&self.uri
}
fn blocked_registries(&self) -> Vec<(Range, HostClass, String)> {
self.blocked.clone()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
let name_range = Range::new(Position::new(0, 0), Position::new(0, 14));
let formatter = MockFormatter;
let parse_result = BlockedRegistryParseResult {
deps: vec![MockDep {
name: "internal-crate".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 20), Position::new(0, 25)),
name_range,
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
blocked: vec![(
name_range,
HostClass::CloudMetadata,
"https://169.254.169.254/index".to_string(),
)],
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
let blocked_diagnostic = diagnostics
.iter()
.find(|d| d.message.contains("blocked"))
.expect("expected a blocked-registry diagnostic");
assert_eq!(blocked_diagnostic.range, name_range);
assert_eq!(
blocked_diagnostic.severity,
Some(DiagnosticSeverity::INFORMATION)
);
assert!(blocked_diagnostic.message.contains("169.254.169.254"));
assert!(blocked_diagnostic.message.contains("cloud metadata"));
assert!(
!blocked_diagnostic.message.contains("CloudMetadata"),
"message must use the Display form, not the Debug identifier"
);
}
#[test]
fn test_truncate_for_diagnostic_leaves_short_value_untouched() {
assert_eq!(truncate_for_diagnostic("my-corp", 128), "my-corp");
}
#[test]
fn test_truncate_for_diagnostic_truncates_and_appends_ellipsis() {
let long_value = "a".repeat(200);
let truncated = truncate_for_diagnostic(&long_value, 128);
assert_eq!(truncated.chars().count(), 129); assert!(truncated.ends_with('…'));
assert!(truncated.starts_with(&"a".repeat(128)));
}
#[test]
fn test_truncate_for_diagnostic_never_splits_a_multibyte_character() {
let long_value = "日".repeat(200);
let truncated = truncate_for_diagnostic(&long_value, 128);
assert_eq!(truncated.chars().count(), 129);
}
#[test]
fn test_generate_diagnostics_from_cache_blocked_registry_message_caps_long_raw_value() {
use crate::net_policy::HostClass;
use tower_lsp_server::ls_types::{Position, Range};
struct BlockedRegistryParseResult {
deps: Vec<MockDep>,
uri: Uri,
blocked: Vec<(Range, HostClass, String)>,
}
impl ParseResult for BlockedRegistryParseResult {
fn dependencies(&self) -> Vec<&dyn Dependency> {
self.deps.iter().map(|d| d as &dyn Dependency).collect()
}
fn workspace_root(&self) -> Option<&std::path::Path> {
None
}
fn uri(&self) -> &Uri {
&self.uri
}
fn blocked_registries(&self) -> Vec<(Range, HostClass, String)> {
self.blocked.clone()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
let name_range = Range::new(Position::new(0, 0), Position::new(0, 14));
let formatter = MockFormatter;
let long_alias = "x".repeat(10_000);
let parse_result = BlockedRegistryParseResult {
deps: vec![MockDep {
name: "internal-crate".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 20), Position::new(0, 25)),
name_range,
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
blocked: vec![(name_range, HostClass::InternalName, long_alias.clone())],
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
let blocked_diagnostic = diagnostics
.iter()
.find(|d| d.message.contains("blocked"))
.expect("expected a blocked-registry diagnostic");
assert!(
blocked_diagnostic.message.len() < long_alias.len(),
"a 10,000-char alias must not render in full inside the diagnostic message"
);
assert!(blocked_diagnostic.message.contains('…'));
}
#[test]
fn test_generate_diagnostics_from_cache_fetch_failed_not_reported_as_unknown() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "flaky-pkg".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 11)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let outcomes =
DependencyOutcomes::new().with_fetch_failure("flaky-pkg", FetchFailure::Transient);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(diagnostics.len(), 1);
assert!(!diagnostics[0].message.contains("Unknown package"));
assert!(diagnostics[0].message.contains("Registry lookup failed"));
assert!(diagnostics[0].message.contains("flaky-pkg"));
}
#[test]
fn test_generate_diagnostics_from_cache_offline_suppresses_per_dependency_warning() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![
MockDep {
name: "flaky-pkg-a".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 11)),
},
MockDep {
name: "flaky-pkg-b".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(1, 10), Position::new(1, 20)),
name_range: Range::new(Position::new(1, 0), Position::new(1, 11)),
},
],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let outcomes = DependencyOutcomes::new()
.with_fetch_failure("flaky-pkg-a", FetchFailure::Transient)
.with_fetch_failure("flaky-pkg-b", FetchFailure::Transient);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions)
.with_outcomes(&outcomes)
.with_offline(true),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
!diagnostics
.iter()
.any(|d| d.message.contains("Registry lookup failed")),
"the per-dependency WARNING must not fire while offline, got: {diagnostics:?}"
);
let offline_diagnostics: Vec<_> = diagnostics
.iter()
.filter(|d| d.severity == Some(DiagnosticSeverity::INFORMATION))
.filter(|d| d.message.to_lowercase().contains("offline"))
.collect();
assert_eq!(
offline_diagnostics.len(),
1,
"expected exactly one file-level offline diagnostic, not one per dependency; \
got: {diagnostics:?}"
);
}
#[test]
fn test_generate_diagnostics_from_cache_offline_signal_present_even_with_no_fetch_failures() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "=1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("serde".into(), PackageVersions::latest_only("1.0.0"));
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_offline(true),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
diagnostics
.iter()
.any(|d| d.severity == Some(DiagnosticSeverity::INFORMATION)
&& d.message.to_lowercase().contains("offline")),
"expected a file-level offline diagnostic even with zero fetch failures; \
got: {diagnostics:?}"
);
}
#[test]
fn test_generate_diagnostics_from_cache_fetch_failed_actionable_shows_hint() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "rate-limited-pkg".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 16)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let outcomes = DependencyOutcomes::new().with_fetch_failure(
"rate-limited-pkg",
FetchFailure::Actionable("set GITHUB_TOKEN to increase the rate limit".to_string()),
);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(diagnostics.len(), 1);
assert!(!diagnostics[0].message.contains("Unknown package"));
assert!(
diagnostics[0]
.message
.contains("set GITHUB_TOKEN to increase the rate limit")
);
assert!(diagnostics[0].message.contains("rate-limited-pkg"));
}
#[test]
fn test_generate_diagnostics_from_cache_multiple_fetch_failed_collapse_into_one_diagnostic() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let name_range_1 = Range::new(Position::new(0, 0), Position::new(0, 8));
let name_range_2 = Range::new(Position::new(1, 0), Position::new(1, 8));
let name_range_3 = Range::new(Position::new(2, 0), Position::new(2, 8));
let parse_result = MockParseResult {
deps: vec![
MockDep {
name: "flaky-1".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: name_range_1,
},
MockDep {
name: "flaky-2".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(1, 10), Position::new(1, 20)),
name_range: name_range_2,
},
MockDep {
name: "flaky-3".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(2, 10), Position::new(2, 20)),
name_range: name_range_3,
},
],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let outcomes = DependencyOutcomes::new()
.with_fetch_failure("flaky-1", FetchFailure::Transient)
.with_fetch_failure("flaky-2", FetchFailure::Transient)
.with_fetch_failure("flaky-3", FetchFailure::Transient);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(
diagnostics.len(),
1,
"3 fetch-failed dependencies must collapse into exactly one diagnostic, got: {diagnostics:?}"
);
assert!(
diagnostics[0]
.message
.contains("Registry lookup failed for 3 packages")
);
assert_eq!(
diagnostics[0].range, name_range_1,
"the combined diagnostic must sit on the first failing dependency's range"
);
let related_information = diagnostics[0]
.related_information
.as_ref()
.expect("collapsed diagnostic must carry related_information for the dropped deps");
assert_eq!(
related_information.len(),
2,
"expected one related_information entry per additional failing dependency (n - 1)"
);
assert_eq!(related_information[0].location.range, name_range_2);
assert_eq!(related_information[0].location.uri, *parse_result.uri());
assert!(related_information[0].message.contains("flaky-2"));
assert_eq!(related_information[1].location.range, name_range_3);
assert_eq!(related_information[1].location.uri, *parse_result.uri());
assert!(related_information[1].message.contains("flaky-3"));
}
#[test]
fn test_generate_diagnostics_from_cache_two_fetch_failed_collapse_boundary() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let name_range_1 = Range::new(Position::new(0, 0), Position::new(0, 8));
let name_range_2 = Range::new(Position::new(1, 0), Position::new(1, 8));
let parse_result = MockParseResult {
deps: vec![
MockDep {
name: "flaky-1".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: name_range_1,
},
MockDep {
name: "flaky-2".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(1, 10), Position::new(1, 20)),
name_range: name_range_2,
},
],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let outcomes = DependencyOutcomes::new()
.with_fetch_failure("flaky-1", FetchFailure::Transient)
.with_fetch_failure("flaky-2", FetchFailure::Transient);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(
diagnostics.len(),
1,
"2 fetch-failed dependencies must already collapse into one diagnostic, got: {diagnostics:?}"
);
assert!(
diagnostics[0]
.message
.contains("Registry lookup failed for 2 packages")
);
let related_information = diagnostics[0]
.related_information
.as_ref()
.expect("the n==2 collapse must already carry related_information");
assert_eq!(
related_information.len(),
1,
"n==2 collapse must carry exactly one related_information entry (n - 1)"
);
assert_eq!(related_information[0].location.range, name_range_2);
assert_eq!(related_information[0].location.uri, *parse_result.uri());
assert!(related_information[0].message.contains("flaky-2"));
}
#[test]
fn test_generate_diagnostics_from_cache_multiple_fetch_failed_shared_actionable_hint() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let name_range_1 = Range::new(Position::new(0, 0), Position::new(0, 8));
let name_range_2 = Range::new(Position::new(1, 0), Position::new(1, 8));
let name_range_3 = Range::new(Position::new(2, 0), Position::new(2, 8));
let parse_result = MockParseResult {
deps: vec![
MockDep {
name: "flaky-1".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: name_range_1,
},
MockDep {
name: "flaky-2".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(1, 10), Position::new(1, 20)),
name_range: name_range_2,
},
MockDep {
name: "flaky-3".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(2, 10), Position::new(2, 20)),
name_range: name_range_3,
},
],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let shared_hint = "set GITHUB_TOKEN to increase the rate limit".to_string();
let outcomes = DependencyOutcomes::new()
.with_fetch_failure("flaky-1", FetchFailure::Actionable(shared_hint.clone()))
.with_fetch_failure("flaky-2", FetchFailure::Actionable(shared_hint.clone()))
.with_fetch_failure("flaky-3", FetchFailure::Actionable(shared_hint.clone()));
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(
diagnostics.len(),
1,
"3 fetch-failed dependencies must still collapse into exactly one diagnostic, got: {diagnostics:?}"
);
assert!(
diagnostics[0].message.contains(&shared_hint),
"collapsed diagnostic must surface the shared actionable hint, got: {}",
diagnostics[0].message
);
assert!(
!diagnostics[0]
.message
.contains("package status could not be determined"),
"the actionable hint must replace, not accompany, the generic fallback message"
);
let related_information = diagnostics[0]
.related_information
.as_ref()
.expect("collapsed diagnostic must carry related_information for the dropped deps");
assert_eq!(
related_information.len(),
2,
"expected one related_information entry per additional failing dependency (n - 1)"
);
}
#[test]
fn test_generate_diagnostics_from_cache_fetch_failed_transient_shows_generic_message() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "transient-pkg".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 13)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let outcomes =
DependencyOutcomes::new().with_fetch_failure("transient-pkg", FetchFailure::Transient);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(diagnostics.len(), 1);
assert!(!diagnostics[0].message.contains("Unknown package"));
assert!(
diagnostics[0]
.message
.contains("package status could not be determined")
);
}
#[test]
fn test_generate_diagnostics_from_cache_fetch_failed_not_attempted_shows_generic_message() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "collided-pkg".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 12)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let outcomes = DependencyOutcomes::new()
.with_fetch_failure("collided-pkg", FetchFailure::NotAttempted);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(diagnostics.len(), 1);
assert!(!diagnostics[0].message.contains("Unknown package"));
assert!(
diagnostics[0]
.message
.contains("package status could not be determined")
);
}
#[test]
fn test_generate_diagnostics_from_cache_fetch_failed_does_not_mask_invalid_name() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = RejectingFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "bad name".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 11)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let outcomes =
DependencyOutcomes::new().with_fetch_failure("bad name", FetchFailure::Transient);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics[0].message.contains("Invalid package name"));
}
#[test]
fn test_generate_diagnostics_from_cache_invalid_package_name() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = RejectingFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "bad-pkg".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 7)),
}],
uri: crate::test_util::test_uri("/test/package.json"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
assert!(diagnostics[0].message.starts_with("Invalid package name"));
assert!(!diagnostics[0].message.contains("Unknown package"));
}
#[test]
fn test_generate_diagnostics_from_cache_outdated_version() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "1.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("serde".into(), PackageVersions::latest_only("2.0.0"));
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::HINT));
assert!(diagnostics[0].message.contains("Newer version available"));
assert!(diagnostics[0].message.contains("2.0.0"));
}
#[test]
fn test_generate_diagnostics_from_cache_outdated_within_cooldown_appends_context() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "1.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into()]),
yanked: Arc::from(Vec::new()),
published_at: Some(PublishTime::from_unix_secs(
PublishTime::now().as_unix_secs() - 60 * 60,
)),
},
);
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::HINT));
assert_eq!(
diagnostics[0].message,
"Newer version available: 2.0.0 (published 1 hour ago — still within the release cooldown window)"
);
assert!(!diagnostics[0].message.contains("ago ago"));
}
#[test]
fn test_generate_diagnostics_from_cache_outdated_outside_cooldown_plain_message() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "1.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into()]),
yanked: Arc::from(Vec::new()),
published_at: Some(PublishTime::from_unix_secs(
PublishTime::now().as_unix_secs() - 10 * 24 * 60 * 60,
)),
},
);
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].message, "Newer version available: 2.0.0");
}
#[test]
fn test_generate_diagnostics_from_cache_outdated_freshness_disabled_plain_message() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "1.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into()]),
yanked: Arc::from(Vec::new()),
published_at: Some(PublishTime::from_unix_secs(
PublishTime::now().as_unix_secs() - 60 * 60,
)),
},
);
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings {
enabled: false,
..crate::freshness::FreshnessSettings::default()
},
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].message, "Newer version available: 2.0.0");
}
#[test]
fn test_generate_diagnostics_from_cache_outdated_cooldown_boundary_is_exclusive() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
const COOLDOWN_SECS: u64 = 100;
let now = PublishTime::from_unix_secs(10_000);
let published_at_at_boundary =
PublishTime::from_unix_secs(10_000 - COOLDOWN_SECS.cast_signed());
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "1.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into()]),
yanked: Arc::from(Vec::new()),
published_at: Some(published_at_at_boundary),
},
);
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings {
enabled: true,
cooldown_secs: COOLDOWN_SECS,
},
DiagnosticSeverities::default(),
now,
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(
diagnostics[0].message, "Newer version available: 2.0.0",
"age exactly equal to cooldown_secs must not be within cooldown"
);
}
#[test]
fn test_generate_diagnostics_from_cache_outdated_cooldown_boundary_one_second_inside() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
const COOLDOWN_SECS: u64 = 100;
let now = PublishTime::from_unix_secs(10_000);
let published_at_just_inside =
PublishTime::from_unix_secs(10_000 - (COOLDOWN_SECS.cast_signed() - 1));
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "1.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into()]),
yanked: Arc::from(Vec::new()),
published_at: Some(published_at_just_inside),
},
);
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings {
enabled: true,
cooldown_secs: COOLDOWN_SECS,
},
DiagnosticSeverities::default(),
now,
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(
diagnostics[0].message,
"Newer version available: 2.0.0 (published 1 minute ago — still within the release cooldown window)",
"age == cooldown_secs - 1 must be within cooldown"
);
}
#[test]
fn test_generate_diagnostics_from_cache_up_to_date() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "^1.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("serde".into(), PackageVersions::latest_only("1.0.214"));
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
diagnostics.is_empty(),
"Expected no diagnostics for up-to-date dependency"
);
}
#[test]
fn test_generate_diagnostics_from_cache_multiple_deps() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![
MockDep {
name: "serde".into(),
version_req: "^1.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
},
MockDep {
name: "tokio".into(),
version_req: "1.0".into(),
version_range: Range::new(Position::new(1, 10), Position::new(1, 20)),
name_range: Range::new(Position::new(1, 0), Position::new(1, 5)),
},
MockDep {
name: "unknown".into(),
version_req: "1.0".into(),
version_range: Range::new(Position::new(2, 10), Position::new(2, 20)),
name_range: Range::new(Position::new(2, 0), Position::new(2, 7)),
},
],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("serde".into(), PackageVersions::latest_only("1.0.214"));
cached_versions.insert("tokio".into(), PackageVersions::latest_only("2.0.0"));
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(diagnostics.len(), 2);
assert_eq!(diagnostics[0].message, "Newer version available: 2.0.0");
assert_eq!(diagnostics[0].code, None);
assert_eq!(diagnostics[1].message, "Unknown package 'unknown'");
assert_eq!(diagnostics[1].code, None);
}
#[test]
fn test_generate_diagnostics_from_cache_unresolved_emits_no_diagnostic() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockUnresolvedFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "spring-boot-starter".into(),
version_req: "$missing".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/libs.versions.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"spring-boot-starter".into(),
PackageVersions::latest_only("3.2.0"),
);
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
diagnostics.is_empty(),
"Expected no diagnostics for an unresolved requirement, got: {diagnostics:?}"
);
}
#[test]
fn test_generate_diagnostics_from_cache_yanked_uses_configured_severity() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "1.0.5".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let outcomes =
DependencyOutcomes::new().with_yanked("serde", ("1.0.5".into(), RemovalStatus::Yanked));
let severities = DiagnosticSeverities {
yanked: DiagnosticSeverity::ERROR,
..DiagnosticSeverities::default()
};
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
severities,
PublishTime::now(),
);
let yanked_diag = diagnostics
.iter()
.find(|d| d.message.starts_with(formatter.yanked_message()))
.expect("expected a yanked diagnostic");
assert_eq!(yanked_diag.severity, Some(DiagnosticSeverity::ERROR));
assert!(yanked_diag.message.contains("1.0.5"));
}
#[test]
fn test_generate_diagnostics_from_cache_yanked_default_severity_unchanged() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "1.0.5".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let outcomes =
DependencyOutcomes::new().with_yanked("serde", ("1.0.5".into(), RemovalStatus::Yanked));
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
let yanked_diag = diagnostics
.iter()
.find(|d| d.message.starts_with(formatter.yanked_message()))
.expect("expected a yanked diagnostic");
assert_eq!(yanked_diag.severity, Some(DiagnosticSeverity::WARNING));
}
#[test]
fn test_generate_diagnostics_from_cache_no_yanked_map_emits_no_yanked_diagnostic() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "1.0.5".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("serde".into(), PackageVersions::latest_only("1.0.5"));
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
!diagnostics
.iter()
.any(|d| d.message.starts_with(formatter.yanked_message())),
"Expected no yanked diagnostic when `yanked` is None, got: {diagnostics:?}"
);
}
#[test]
fn test_generate_diagnostics_from_cache_yanked_and_outdated_both_emitted() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "1.0.5".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("serde".into(), PackageVersions::latest_only("2.0.0"));
let resolved_versions = HashMap::new();
let outcomes =
DependencyOutcomes::new().with_yanked("serde", ("1.0.5".into(), RemovalStatus::Yanked));
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(
diagnostics.len(),
2,
"expected both diagnostics: {diagnostics:?}"
);
assert_eq!(
diagnostics[0].message,
format!("{} (1.0.5)", formatter.yanked_message())
);
assert_eq!(diagnostics[0].code, None);
assert_eq!(diagnostics[1].message, "Newer version available: 2.0.0");
assert_eq!(diagnostics[1].code, None);
}
#[test]
fn test_generate_diagnostics_from_cache_deprecation_suppresses_advisory_deprecated_yanked_finding()
{
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "left-pad".into(),
version_req: "1.3.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/package.json"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("left-pad".into(), PackageVersions::latest_only("1.3.0"));
let resolved_versions = HashMap::new();
let outcomes = DependencyOutcomes::new()
.with_yanked(
"left-pad",
("1.3.0".into(), RemovalStatus::AdvisoryDeprecated),
)
.with_deprecation(
"left-pad",
Deprecation {
reason: Some("use String.prototype.padStart()".to_string()),
replacement: None,
},
);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(
diagnostics.len(),
1,
"the deprecation diagnostic must suppress the AdvisoryDeprecated yanked \
finding, not double-report: {diagnostics:?}"
);
assert!(
diagnostics[0]
.message
.starts_with(formatter.deprecated_message())
);
}
#[test]
fn test_generate_diagnostics_from_cache_deprecation_never_suppresses_real_yanked_finding() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "pkg".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/pyproject.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("pkg".into(), PackageVersions::latest_only("1.0.0"));
let resolved_versions = HashMap::new();
let outcomes = DependencyOutcomes::new()
.with_yanked("pkg", ("1.0.0".into(), RemovalStatus::Yanked))
.with_deprecation(
"pkg",
Deprecation {
reason: Some("project archived".to_string()),
replacement: None,
},
);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(
diagnostics.len(),
2,
"a genuine Yanked finding must never be hidden behind a package-level \
deprecation notice: {diagnostics:?}"
);
assert_eq!(
diagnostics[0].message,
format!("{}: project archived", formatter.deprecated_message())
);
assert_eq!(
diagnostics[0].code,
Some(NumberOrString::String(DEPRECATED_DIAGNOSTIC_CODE.into()))
);
assert_eq!(
diagnostics[1].message,
format!("{} (1.0.0)", formatter.yanked_message())
);
assert_eq!(diagnostics[1].code, None);
}
#[test]
fn test_generate_diagnostics_from_cache_deprecation_suppression_is_severity_independent() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "left-pad".into(),
version_req: "1.3.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/package.json"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("left-pad".into(), PackageVersions::latest_only("1.3.0"));
let resolved_versions = HashMap::new();
let outcomes = DependencyOutcomes::new()
.with_yanked(
"left-pad",
("1.3.0".into(), RemovalStatus::AdvisoryDeprecated),
)
.with_deprecation(
"left-pad",
Deprecation {
reason: Some("use String.prototype.padStart()".to_string()),
replacement: None,
},
);
let versions =
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes);
let default_severities = DiagnosticSeverities::default();
let inverted_severities = DiagnosticSeverities {
deprecated: DiagnosticSeverity::HINT,
yanked: DiagnosticSeverity::ERROR,
..DiagnosticSeverities::default()
};
for severities in [default_severities, inverted_severities] {
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
versions,
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
severities,
PublishTime::now(),
);
assert_eq!(
diagnostics.len(),
1,
"suppression must not depend on severity configuration: {diagnostics:?}"
);
assert!(
diagnostics[0]
.message
.starts_with(formatter.deprecated_message())
);
}
}
#[test]
fn test_generate_diagnostics_from_cache_deprecation_skipped_for_non_registry_sources() {
use crate::parser::DependencySource;
let mut cached_versions = HashMap::new();
cached_versions.insert("dep".into(), PackageVersions::latest_only("1.0.0"));
let resolved_versions = HashMap::new();
let outcomes = DependencyOutcomes::new().with_deprecation(
"dep",
Deprecation {
reason: Some("archived".to_string()),
replacement: None,
},
);
let uri = crate::test_util::test_uri("/test/Cargo.toml");
for source in [
DependencySource::Path {
path: "../local".into(),
},
DependencySource::Git {
url: "https://example.com/repo.git".into(),
rev: None,
},
DependencySource::Sdk {
sdk: "flutter".into(),
},
DependencySource::Workspace,
] {
let parse_result = SingleDepParseResult {
dep: NonRegistryDep(dep_at("dep"), source.clone()),
uri: uri.clone(),
};
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&MockFormatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
diagnostics
.iter()
.all(|d| !d.message.starts_with(MockFormatter.deprecated_message())),
"source {source:?} must never surface the deprecation diagnostic for a \
coincidentally-named registry package: {diagnostics:?}"
);
}
let registry_parse_result = SingleDepParseResult {
dep: dep_at("dep"),
uri,
};
let diagnostics = generate_diagnostics_from_cache(
®istry_parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&MockFormatter,
registry_parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
diagnostics
.iter()
.any(|d| d.message.starts_with(MockFormatter.deprecated_message())),
"control case: a Registry-source dependency must still produce the diagnostic"
);
}
#[test]
fn test_generate_diagnostics_from_cache_yanked_no_version_range_uses_name_range() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let name_range = Range::new(Position::new(0, 0), Position::new(0, 5));
let parse_result = MockMarkedParseResult {
dep: MockMarkedDep {
name: "serde".into(),
name_range,
markers: None,
},
uri: crate::test_util::test_uri("/test/pyproject.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let outcomes =
DependencyOutcomes::new().with_yanked("serde", ("1.0.5".into(), RemovalStatus::Yanked));
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
let yanked_diag = diagnostics
.iter()
.find(|d| d.message.starts_with(formatter.yanked_message()))
.expect("expected a yanked diagnostic even without a version_range");
assert_eq!(yanked_diag.range, name_range);
}
#[test]
fn test_generate_diagnostics_from_cache_yanked_normalized_name_keying() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
struct MockLowercaseFormatter;
impl PackageNaming for MockLowercaseFormatter {
fn normalize_package_name(&self, name: &PackageName) -> String {
name.to_string().to_lowercase()
}
}
impl PackageRendering for MockLowercaseFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
format!("https://example.com/{name}")
}
}
impl RequirementResolution for MockLowercaseFormatter {}
impl DiagnosticMessages for MockLowercaseFormatter {}
impl DiagnosticPolicy for MockLowercaseFormatter {}
impl SourcePolicy for MockLowercaseFormatter {}
impl OsvNaming for MockLowercaseFormatter {}
let formatter = MockLowercaseFormatter;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "Newtonsoft.Json".into(),
version_req: "13.0.1".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/project.csproj"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let outcomes = DependencyOutcomes::new()
.with_yanked("newtonsoft.json", ("13.0.1".into(), RemovalStatus::Yanked));
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
diagnostics
.iter()
.any(|d| d.message.starts_with(formatter.yanked_message())),
"expected normalized-name lookup to resolve the yanked entry, got: {diagnostics:?}"
);
}
#[test]
fn test_generate_diagnostics_from_cache_yanked_not_shared_across_duplicate_name_occurrences() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![
MockDep {
name: "time".into(),
version_req: "=0.1.43".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
},
MockDep {
name: "time".into(),
version_req: "=0.1.44".into(),
version_range: Range::new(Position::new(3, 10), Position::new(3, 20)),
name_range: Range::new(Position::new(3, 0), Position::new(3, 5)),
},
],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let outcomes =
DependencyOutcomes::new().with_yanked("time", ("0.1.43".into(), RemovalStatus::Yanked));
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions)
.with_outcomes(&outcomes)
.with_ecosystem(crate::EcosystemId::Cargo),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
let yanked_diags: Vec<_> = diagnostics
.iter()
.filter(|d| d.message.starts_with(formatter.yanked_message()))
.collect();
assert_eq!(
yanked_diags.len(),
1,
"exactly one occurrence must get the yanked diagnostic, got: {diagnostics:?}"
);
assert_eq!(
yanked_diags[0].range.start.line, 0,
"must land on the yanked occurrence's own line"
);
assert!(yanked_diags[0].message.contains("0.1.43"));
}
#[test]
fn test_generate_diagnostics_from_cache_yanked_no_ecosystem_keeps_pre_394_behavior() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![
MockDep {
name: "time".into(),
version_req: "=0.1.43".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
},
MockDep {
name: "time".into(),
version_req: "=0.1.44".into(),
version_range: Range::new(Position::new(3, 10), Position::new(3, 20)),
name_range: Range::new(Position::new(3, 0), Position::new(3, 5)),
},
],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let outcomes =
DependencyOutcomes::new().with_yanked("time", ("0.1.43".into(), RemovalStatus::Yanked));
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
let yanked_diags = diagnostics
.iter()
.filter(|d| d.message.starts_with(formatter.yanked_message()))
.count();
assert_eq!(
yanked_diags, 2,
"no `ecosystem` set: both occurrences share the finding as before #394"
);
}
#[test]
fn test_generate_diagnostics_unsatisfiable_enriched_with_matching_prerelease() {
let cached_versions = {
let mut m = HashMap::new();
m.insert(
"dep".into(),
PackageVersions {
latest: "1.5.0".into(),
available: Arc::from(vec!["2.0.0-rc.1".into(), "1.5.0".into(), "1.4.0".into()]),
yanked: Arc::from(Vec::new()),
published_at: None,
},
);
m
};
let resolved_versions = HashMap::new();
let uri = crate::test_util::test_uri("/test/Cargo.toml");
let mut dependency = dep_at("dep");
dependency.version_req = VersionReq::new("^2.0.0");
let parse_result = SingleDepParseResult {
dep: dependency,
uri,
};
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&StrictSemverFormatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
let message = diagnostics
.iter()
.find(|d| d.message.contains("No published version satisfies"))
.map(|d| d.message.as_str())
.expect("unsatisfiable WARNING must fire");
assert!(
message.contains("2.0.0-rc.1") && message.contains("pre-release"),
"message must mention the matching pre-release, got: {message}"
);
}
#[test]
fn test_generate_diagnostics_unsatisfiable_no_enrichment_without_matching_prerelease() {
let cached_versions = {
let mut m = HashMap::new();
m.insert(
"dep".into(),
PackageVersions {
latest: "1.5.0".into(),
available: Arc::from(vec!["1.5.0".into(), "1.4.0".into()]),
yanked: Arc::from(Vec::new()),
published_at: None,
},
);
m
};
let resolved_versions = HashMap::new();
let uri = crate::test_util::test_uri("/test/Cargo.toml");
let mut dependency = dep_at("dep");
dependency.version_req = VersionReq::new("^2.0.0");
let parse_result = SingleDepParseResult {
dep: dependency,
uri,
};
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&StrictSemverFormatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
let message = diagnostics
.iter()
.find(|d| d.message.contains("No published version satisfies"))
.map(|d| d.message.as_str())
.expect("unsatisfiable WARNING must fire");
assert!(
!message.contains("pre-release"),
"message must not mention a pre-release when none satisfies, got: {message}"
);
}
#[test]
fn test_generate_diagnostics_unsatisfiable_skipped_for_non_registry_sources() {
use crate::parser::DependencySource;
let cached_versions = {
let mut m = HashMap::new();
m.insert("dep".into(), PackageVersions::latest_only("9.9.9"));
m
};
let resolved_versions = HashMap::new();
let uri = crate::test_util::test_uri("/test/Cargo.toml");
for source in [
DependencySource::Path {
path: "../local".into(),
},
DependencySource::Git {
url: "https://example.com/repo.git".into(),
rev: None,
},
DependencySource::Url {
url: "https://example.com/pkg.tar.gz".into(),
},
DependencySource::Sdk {
sdk: "flutter".into(),
},
DependencySource::Workspace,
DependencySource::CustomRegistry {
url: "my-corp".into(),
},
] {
let parse_result = SingleDepParseResult {
dep: NonRegistryDep(dep_at("dep"), source.clone()),
uri: uri.clone(),
};
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&ExactMatchFormatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
diagnostics
.iter()
.all(|d| !d.message.contains("No published version satisfies")),
"source {source:?} must never produce the unsatisfiable-requirement WARNING"
);
}
let registry_parse_result = SingleDepParseResult {
dep: dep_at("dep"),
uri,
};
let diagnostics = generate_diagnostics_from_cache(
®istry_parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&ExactMatchFormatter,
registry_parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
diagnostics
.iter()
.any(|d| d.message.contains("No published version satisfies")),
"control case: a Registry-source dependency must still produce the WARNING"
);
}
#[test]
fn test_generate_diagnostics_unknown_package_skipped_for_non_registry_sources() {
use crate::parser::DependencySource;
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let uri = crate::test_util::test_uri("/test/Cargo.toml");
let parse_result = SingleDepParseResult {
dep: NonRegistryDep(
dep_at("dep"),
DependencySource::CustomRegistry {
url: "my-corp".into(),
},
),
uri: uri.clone(),
};
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&MockFormatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
diagnostics
.iter()
.all(|d| !d.message.contains("Unknown package")),
"a CustomRegistry-sourced dependency must never produce the \"Unknown package\" WARNING"
);
let registry_parse_result = SingleDepParseResult {
dep: dep_at("dep"),
uri,
};
let diagnostics = generate_diagnostics_from_cache(
®istry_parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&MockFormatter,
registry_parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
diagnostics
.iter()
.any(|d| d.message.contains("Unknown package")),
"control case: a Registry-source dependency must still produce the WARNING"
);
}
#[test]
fn test_generate_diagnostics_invalid_name_still_reported_for_non_registry_sources() {
use crate::parser::DependencySource;
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let uri = crate::test_util::test_uri("/test/package.json");
let parse_result = SingleDepParseResult {
dep: NonRegistryDep(
dep_at("dep"),
DependencySource::CustomRegistry {
url: "my-corp".into(),
},
),
uri,
};
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&RejectingFormatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics[0].message.starts_with("Invalid package name"));
}
#[test]
fn test_generate_diagnostics_outdated_skipped_for_non_registry_sources() {
use crate::parser::DependencySource;
let cached_versions = {
let mut m = HashMap::new();
m.insert("dep".into(), PackageVersions::latest_only("9.9.9"));
m
};
let resolved_versions = HashMap::new();
let uri = crate::test_util::test_uri("/test/Cargo.toml");
let parse_result = SingleDepParseResult {
dep: NonRegistryDep(
dep_at("dep"),
DependencySource::CustomRegistry {
url: "my-corp".into(),
},
),
uri: uri.clone(),
};
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&MockFormatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
diagnostics
.iter()
.all(|d| !d.message.contains("Newer version available")),
"a CustomRegistry-sourced dependency must never produce the \"Outdated\" WARNING"
);
let registry_parse_result = SingleDepParseResult {
dep: dep_at("dep"),
uri,
};
let diagnostics = generate_diagnostics_from_cache(
®istry_parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&MockFormatter,
registry_parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
diagnostics
.iter()
.any(|d| d.message.contains("Newer version available")),
"control case: a Registry-source dependency must still produce the WARNING"
);
}
#[test]
fn test_generate_diagnostics_vulnerable_dependency_emits_advisory_diagnostic_even_without_registry_data()
{
use crate::osv::{
Capped, DependencyVulnerabilities, ScanOutcome, UpgradeStatus, VulnSeverity,
VulnerabilityMap,
};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![dep_at("vulnerable-pkg")],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
"vulnerable-pkg".to_string(),
ScanOutcome::Vulnerable(DependencyVulnerabilities {
advisories: Capped::new(
vec![sample_advisory("RUSTSEC-2020-0071", VulnSeverity::High)],
1,
),
fix_target_status: UpgradeStatus::NotChecked,
upgrade_status: UpgradeStatus::NotChecked,
}),
);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_vulnerabilities(&vulns),
&formatter,
parse_result.uri(),
crate::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
let vuln_diag = diagnostics
.iter()
.find(|d| d.message.contains("RUSTSEC-2020-0071"))
.expect("vulnerability diagnostic must be emitted even without registry data");
assert_eq!(vuln_diag.severity, Some(DiagnosticSeverity::WARNING));
assert_eq!(
vuln_diag.code,
Some(NumberOrString::String("RUSTSEC-2020-0071".to_string()))
);
}
#[test]
fn test_generate_diagnostics_advisory_cap_emits_more_count_from_total_known() {
use crate::osv::{
ADVISORY_DISPLAY_CAP, Capped, DependencyVulnerabilities, ScanOutcome, UpgradeStatus,
VulnSeverity, VulnerabilityMap,
};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![dep_at("noisy-pkg")],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let advisories: Vec<_> = (0..ADVISORY_DISPLAY_CAP)
.map(|i| sample_advisory(&format!("ADV-{i}"), VulnSeverity::Low))
.collect();
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
"noisy-pkg".to_string(),
ScanOutcome::Vulnerable(DependencyVulnerabilities {
advisories: Capped::new(advisories, 40),
fix_target_status: UpgradeStatus::NotChecked,
upgrade_status: UpgradeStatus::NotChecked,
}),
);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_vulnerabilities(&vulns),
&formatter,
parse_result.uri(),
crate::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
let more_diag = diagnostics
.iter()
.find(|d| d.message.contains("more advisories"))
.expect("expected a trailing +N more advisories diagnostic");
assert!(
more_diag.message.contains("+35"),
"got: {}",
more_diag.message
);
}
#[test]
fn test_generate_diagnostics_vulnerability_not_shared_across_duplicate_name_occurrences() {
use crate::osv::{
Capped, DependencyVulnerabilities, ScanOutcome, UpgradeStatus, VulnSeverity,
VulnerabilityMap, vulnerability_keys,
};
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let vulnerable_dep = MockDep {
name: "pkg".into(),
version_req: "=1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
};
let patched_dep = MockDep {
name: "pkg".into(),
version_req: "=2.0.0".into(),
version_range: Range::new(Position::new(3, 10), Position::new(3, 20)),
name_range: Range::new(Position::new(3, 0), Position::new(3, 5)),
};
let parse_result = MockParseResult {
deps: vec![vulnerable_dep, patched_dep],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let keys = vulnerability_keys(
&parse_result,
&resolved_versions,
&formatter,
crate::EcosystemId::Cargo,
);
let deps = parse_result.dependencies();
let vulnerable_key = keys.get(&deps[0].name_range()).unwrap().clone();
let patched_key = keys.get(&deps[1].name_range()).unwrap().clone();
assert_ne!(
vulnerable_key, patched_key,
"differently-versioned occurrences of one name must get distinct keys"
);
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
vulnerable_key,
ScanOutcome::Vulnerable(DependencyVulnerabilities {
advisories: Capped::new(
vec![sample_advisory("RUSTSEC-2020-0071", VulnSeverity::High)],
1,
),
fix_target_status: UpgradeStatus::NotChecked,
upgrade_status: UpgradeStatus::NotChecked,
}),
);
vulns.insert(patched_key, ScanOutcome::Clean);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions)
.with_vulnerabilities(&vulns)
.with_ecosystem(crate::EcosystemId::Cargo),
&formatter,
parse_result.uri(),
crate::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
let advisory_diags: Vec<_> = diagnostics
.iter()
.filter(|d| d.message.contains("RUSTSEC-2020-0071"))
.collect();
assert_eq!(
advisory_diags.len(),
1,
"exactly one occurrence must get the advisory diagnostic, got: {diagnostics:?}"
);
assert_eq!(
advisory_diags[0].range.start.line, 0,
"must land on the vulnerable occurrence's own line, not the patched one"
);
}
#[test]
fn test_generate_diagnostics_skipped_outcome_emits_no_vulnerability_diagnostic() {
use crate::osv::{ScanOutcome, SkipReason, VulnerabilityMap};
let formatter = MockFormatter;
let parse_result = MockParseResult {
deps: vec![dep_at("git-pkg")],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("git-pkg".into(), PackageVersions::latest_only("1.0.0"));
let resolved_versions = HashMap::new();
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
"git-pkg".to_string(),
ScanOutcome::Skipped(SkipReason::NonRegistrySource),
);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_vulnerabilities(&vulns),
&formatter,
parse_result.uri(),
crate::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
diagnostics.iter().all(|d| d.code.is_none()),
"a Skipped outcome must never render an advisory diagnostic"
);
}
mod requirement_is_unsatisfiable_tests {
use super::*;
type Decide = Arc<dyn Fn(&str) -> Option<bool> + Send + Sync>;
struct ClosureMatcher(Decide);
impl RequirementMatcher for ClosureMatcher {
fn matches(&self, version: &ConcreteVersion) -> Option<bool> {
(self.0)(version.as_str())
}
}
struct TableFormatter {
decide: Decide,
}
impl TableFormatter {
fn new(decide: impl Fn(&str) -> Option<bool> + Send + Sync + 'static) -> Self {
Self {
decide: Arc::new(decide),
}
}
}
impl PackageNaming for TableFormatter {}
impl PackageRendering for TableFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
name.to_string()
}
}
impl RequirementResolution for TableFormatter {
fn requirement_is_unresolved(&self, requirement: &VersionReq) -> bool {
requirement.as_str() == "unresolved"
}
fn compile_requirement(
&self,
requirement: &VersionReq,
) -> Option<Box<dyn RequirementMatcher>> {
if requirement.as_str() != "modelled" {
return None;
}
Some(Box::new(ClosureMatcher(Arc::clone(&self.decide)))
as Box<dyn RequirementMatcher>)
}
}
impl DiagnosticMessages for TableFormatter {}
impl DiagnosticPolicy for TableFormatter {}
impl SourcePolicy for TableFormatter {}
impl OsvNaming for TableFormatter {}
fn versions(strs: &[&str]) -> Vec<ConcreteVersion> {
strs.iter().map(|s| (*s).into()).collect()
}
#[test]
fn test_empty_available_list_is_false() {
let formatter = TableFormatter::new(|_v| Some(true));
assert!(!requirement_is_unsatisfiable(
&formatter,
&VersionReq::new("modelled"),
&[],
));
}
#[test]
fn test_empty_requirement_string_is_false() {
let formatter = TableFormatter::new(|_v| Some(false));
assert!(!requirement_is_unsatisfiable(
&formatter,
&VersionReq::new(""),
&versions(&["1.0.0"]),
));
}
#[test]
fn test_oversized_requirement_is_false_without_compiling() {
let formatter =
TableFormatter::new(|_v| panic!("must not compile/scan an oversized requirement"));
let oversized = "1".repeat(MAX_REQUIREMENT_LEN + 1);
assert!(!requirement_is_unsatisfiable(
&formatter,
&VersionReq::new(oversized),
&versions(&["1.0.0"]),
));
}
#[test]
fn test_unresolved_requirement_is_false() {
let formatter = TableFormatter::new(|_v| Some(false));
assert!(!requirement_is_unsatisfiable(
&formatter,
&VersionReq::new("unresolved"),
&versions(&["1.0.0"]),
));
}
#[test]
fn test_compile_requirement_none_is_false() {
let formatter = TableFormatter::new(|_v| Some(false));
assert!(!requirement_is_unsatisfiable(
&formatter,
&VersionReq::new("not-modelled"),
&versions(&["1.0.0"]),
));
}
#[test]
fn test_all_candidates_decided_false_is_true() {
let formatter = TableFormatter::new(|_v| Some(false));
assert!(requirement_is_unsatisfiable(
&formatter,
&VersionReq::new("modelled"),
&versions(&["1.0.0", "2.0.0", "3.0.0"]),
));
}
#[test]
fn test_one_match_among_many_non_matches_is_false() {
let formatter = TableFormatter::new(|v| Some(v == "2.0.0"));
assert!(!requirement_is_unsatisfiable(
&formatter,
&VersionReq::new("modelled"),
&versions(&["1.0.0", "2.0.0", "3.0.0"]),
));
}
#[test]
fn test_all_candidates_unparseable_is_false() {
let formatter = TableFormatter::new(|_v| None);
assert!(!requirement_is_unsatisfiable(
&formatter,
&VersionReq::new("modelled"),
&versions(&["1.0.0", "2.0.0"]),
));
}
#[test]
fn test_one_unparseable_candidate_among_false_is_still_true() {
let formatter = TableFormatter::new(|v| if v == "junk" { None } else { Some(false) });
assert!(requirement_is_unsatisfiable(
&formatter,
&VersionReq::new("modelled"),
&versions(&["1.0.0", "junk", "2.0.0"]),
));
}
#[test]
fn test_match_on_yanked_only_candidate_is_false() {
let formatter = TableFormatter::new(|v| Some(v == "1.0.0-yanked"));
assert!(!requirement_is_unsatisfiable(
&formatter,
&VersionReq::new("modelled"),
&versions(&["1.0.0-yanked"]),
));
}
#[test]
fn test_match_on_prerelease_only_candidate_is_false() {
let formatter = TableFormatter::new(|v| Some(v == "2.0.0-beta.1"));
assert!(!requirement_is_unsatisfiable(
&formatter,
&VersionReq::new("modelled"),
&versions(&["2.0.0-beta.1"]),
));
}
#[test]
fn test_scan_short_circuits_on_first_match() {
let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let calls_clone = Arc::clone(&calls);
let formatter = TableFormatter::new(move |v| {
calls_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Some(v == "1.0.0")
});
let result = requirement_is_unsatisfiable(
&formatter,
&VersionReq::new("modelled"),
&versions(&["1.0.0", "0.9.0", "0.8.0"]),
);
assert!(!result);
assert_eq!(
calls.load(std::sync::atomic::Ordering::SeqCst),
1,
"must stop scanning at the first Some(true)"
);
}
}
mod matching_prerelease_would_satisfy_tests {
use super::*;
struct NonStrictFormatter;
impl PackageNaming for NonStrictFormatter {}
impl PackageRendering for NonStrictFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
name.to_string()
}
}
impl RequirementResolution for NonStrictFormatter {
fn compile_requirement(
&self,
requirement: &VersionReq,
) -> Option<Box<dyn RequirementMatcher>> {
requirement
.as_str()
.parse::<semver::VersionReq>()
.ok()
.map(|req| Box::new(RealSemverMatcher(req)) as Box<dyn RequirementMatcher>)
}
}
impl DiagnosticMessages for NonStrictFormatter {}
impl DiagnosticPolicy for NonStrictFormatter {}
impl SourcePolicy for NonStrictFormatter {}
impl OsvNaming for NonStrictFormatter {}
fn versions(strs: &[&str]) -> Vec<ConcreteVersion> {
strs.iter().map(|s| (*s).into()).collect()
}
fn yanked_versions(strs: &[&str]) -> Vec<(ConcreteVersion, RemovalStatus)> {
strs.iter()
.map(|s| ((*s).into(), RemovalStatus::Yanked))
.collect()
}
#[test]
fn test_semver_prerelease_base() {
assert_eq!(semver_prerelease_base("2.0.0-rc.1"), Some("2.0.0"));
assert_eq!(semver_prerelease_base("2.0.0-rc.1+build.5"), Some("2.0.0"));
assert_eq!(semver_prerelease_base("2.0.0"), None);
assert_eq!(semver_prerelease_base("2.0.0+build.5"), None);
}
#[test]
fn test_requirement_names_prerelease() {
assert!(requirement_names_prerelease("2.0.0-rc.5"));
assert!(requirement_names_prerelease("^2.0.0-rc.5"));
assert!(requirement_names_prerelease("~2.0.0-rc.5"));
assert!(requirement_names_prerelease(">=2.0.0-rc.5"));
assert!(requirement_names_prerelease("=2.0.0-rc.5"));
assert!(!requirement_names_prerelease("^2.0.0"));
assert!(!requirement_names_prerelease(">=1.0.0, <2.0.0"));
assert!(!requirement_names_prerelease("1.2.3 - 2.3.4"));
}
#[test]
fn test_no_prerelease_available_returns_none() {
assert_eq!(
matching_prerelease_would_satisfy(
&StrictSemverFormatter,
&VersionReq::new("^2.0.0"),
&versions(&["1.5.0", "1.4.0"]),
&[],
),
None
);
}
#[test]
fn test_matching_prerelease_is_found() {
assert_eq!(
matching_prerelease_would_satisfy(
&StrictSemverFormatter,
&VersionReq::new("^2.0.0"),
&versions(&["2.0.0-rc.1", "1.5.0"]),
&[],
),
Some("2.0.0-rc.1".to_string())
);
}
#[test]
fn test_returns_newest_matching_prerelease_first() {
assert_eq!(
matching_prerelease_would_satisfy(
&StrictSemverFormatter,
&VersionReq::new("^2.0.0"),
&versions(&["2.0.0-rc.2", "2.0.0-rc.1", "1.5.0"]),
&[],
),
Some("2.0.0-rc.2".to_string())
);
}
#[test]
fn test_non_matching_prerelease_returns_none() {
assert_eq!(
matching_prerelease_would_satisfy(
&StrictSemverFormatter,
&VersionReq::new("^2.0.0"),
&versions(&["3.0.0-rc.1", "1.5.0"]),
&[],
),
None
);
}
#[test]
fn test_exact_prerelease_pin_does_not_misfire_on_unrelated_prerelease() {
assert_eq!(
matching_prerelease_would_satisfy(
&StrictSemverFormatter,
&VersionReq::new("=2.0.0-rc.5"),
&versions(&["2.0.0-rc.1", "1.5.0"]),
&[],
),
None
);
}
#[test]
fn test_caret_requirement_naming_prerelease_returns_none() {
assert_eq!(
matching_prerelease_would_satisfy(
&StrictSemverFormatter,
&VersionReq::new("^2.0.0-rc.5"),
&versions(&["2.0.0-rc.1", "1.5.0"]),
&[],
),
None
);
}
#[test]
fn test_tilde_requirement_naming_prerelease_returns_none() {
assert_eq!(
matching_prerelease_would_satisfy(
&StrictSemverFormatter,
&VersionReq::new("~2.0.0-rc.5"),
&versions(&["2.0.0-rc.1", "1.5.0"]),
&[],
),
None
);
}
#[test]
fn test_gte_requirement_naming_prerelease_returns_none() {
assert_eq!(
matching_prerelease_would_satisfy(
&StrictSemverFormatter,
&VersionReq::new(">=2.0.0-rc.5"),
&versions(&["2.0.0-rc.1", "1.5.0"]),
&[],
),
None
);
}
#[test]
fn test_yanked_matching_prerelease_is_skipped() {
assert_eq!(
matching_prerelease_would_satisfy(
&StrictSemverFormatter,
&VersionReq::new("^2.0.0"),
&versions(&["2.0.0-rc.1", "1.5.0"]),
&yanked_versions(&["2.0.0-rc.1"]),
),
None
);
}
#[test]
fn test_yanked_matching_prerelease_falls_back_to_non_yanked() {
assert_eq!(
matching_prerelease_would_satisfy(
&StrictSemverFormatter,
&VersionReq::new("^2.0.0"),
&versions(&["2.0.0-rc.2", "2.0.0-rc.1", "1.5.0"]),
&yanked_versions(&["2.0.0-rc.2"]),
),
Some("2.0.0-rc.1".to_string())
);
}
#[test]
fn test_non_opted_in_ecosystem_returns_none() {
assert_eq!(
matching_prerelease_would_satisfy(
&NonStrictFormatter,
&VersionReq::new("^2.0.0"),
&versions(&["2.0.0-rc.1", "1.5.0"]),
&[],
),
None
);
}
}
mod requirement_matches_only_yanked_tests {
use super::*;
type Decide = Arc<dyn Fn(&str) -> Option<bool> + Send + Sync>;
struct ClosureMatcher(Decide);
impl RequirementMatcher for ClosureMatcher {
fn matches(&self, version: &ConcreteVersion) -> Option<bool> {
(self.0)(version.as_str())
}
}
struct TableFormatter {
decide: Decide,
}
impl TableFormatter {
fn new(decide: impl Fn(&str) -> Option<bool> + Send + Sync + 'static) -> Self {
Self {
decide: Arc::new(decide),
}
}
}
impl PackageNaming for TableFormatter {}
impl PackageRendering for TableFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
name.to_string()
}
}
impl RequirementResolution for TableFormatter {
fn requirement_is_unresolved(&self, requirement: &VersionReq) -> bool {
requirement.as_str() == "unresolved"
}
fn compile_requirement(
&self,
requirement: &VersionReq,
) -> Option<Box<dyn RequirementMatcher>> {
if requirement.as_str() != "modelled" {
return None;
}
Some(Box::new(ClosureMatcher(Arc::clone(&self.decide)))
as Box<dyn RequirementMatcher>)
}
}
impl DiagnosticMessages for TableFormatter {}
impl DiagnosticPolicy for TableFormatter {}
impl SourcePolicy for TableFormatter {}
impl OsvNaming for TableFormatter {}
fn versions(strs: &[&str]) -> Vec<ConcreteVersion> {
strs.iter().map(|s| (*s).into()).collect()
}
fn yanked_versions(strs: &[&str]) -> Vec<(ConcreteVersion, RemovalStatus)> {
strs.iter()
.map(|s| ((*s).into(), RemovalStatus::Yanked))
.collect()
}
#[test]
fn test_yanked_only_match_is_true() {
let formatter = TableFormatter::new(|v| Some(v == "1.2.1"));
assert_eq!(
requirement_matches_only_yanked(
&formatter,
&VersionReq::new("modelled"),
&versions(&["1.2.1"]),
&yanked_versions(&["1.2.1"]),
),
Some(RemovalStatus::Yanked)
);
}
#[test]
fn test_duplicate_version_prefers_yanked_status_regardless_of_order() {
let formatter = TableFormatter::new(|v| Some(v == "1.2.1"));
let advisory_then_yanked = vec![
("1.2.1".into(), RemovalStatus::AdvisoryDeprecated),
("1.2.1".into(), RemovalStatus::Yanked),
];
assert_eq!(
requirement_matches_only_yanked(
&formatter,
&VersionReq::new("modelled"),
&versions(&["1.2.1"]),
&advisory_then_yanked,
),
Some(RemovalStatus::Yanked),
"Yanked must win when it comes after AdvisoryDeprecated in the duplicate list"
);
let yanked_then_advisory = vec![
("1.2.1".into(), RemovalStatus::Yanked),
("1.2.1".into(), RemovalStatus::AdvisoryDeprecated),
];
assert_eq!(
requirement_matches_only_yanked(
&formatter,
&VersionReq::new("modelled"),
&versions(&["1.2.1"]),
&yanked_then_advisory,
),
Some(RemovalStatus::Yanked),
"Yanked must win when it comes before AdvisoryDeprecated in the duplicate list"
);
}
#[test]
fn test_no_match_is_false() {
let formatter = TableFormatter::new(|_v| Some(false));
assert!(
requirement_matches_only_yanked(
&formatter,
&VersionReq::new("modelled"),
&versions(&["1.2.1"]),
&yanked_versions(&["1.2.1"]),
)
.is_none()
);
}
#[test]
fn test_match_on_non_yanked_alongside_yanked_is_false() {
let formatter = TableFormatter::new(|v| Some(v == "1.0.0" || v == "1.0.1"));
assert!(
requirement_matches_only_yanked(
&formatter,
&VersionReq::new("modelled"),
&versions(&["1.0.1", "1.0.0"]),
&yanked_versions(&["1.0.0"]),
)
.is_none()
);
}
#[test]
fn test_scan_continues_past_a_yanked_match_to_find_a_non_yanked_alternative() {
let formatter = TableFormatter::new(|v| Some(v == "1.0.0" || v == "1.0.1"));
assert!(
requirement_matches_only_yanked(
&formatter,
&VersionReq::new("modelled"),
&versions(&["1.0.0", "1.0.1"]),
&yanked_versions(&["1.0.0"]),
)
.is_none()
);
}
#[test]
fn test_empty_yanked_list_is_false_without_compiling() {
let formatter =
TableFormatter::new(|_v| panic!("must not compile/scan when yanked is empty"));
assert!(
requirement_matches_only_yanked(
&formatter,
&VersionReq::new("modelled"),
&versions(&["1.0.0"]),
&[],
)
.is_none()
);
}
#[test]
fn test_empty_available_list_is_false() {
let formatter = TableFormatter::new(|_v| Some(true));
assert!(
requirement_matches_only_yanked(
&formatter,
&VersionReq::new("modelled"),
&[],
&yanked_versions(&["1.0.0"]),
)
.is_none()
);
}
#[test]
fn test_unresolved_requirement_is_false() {
let formatter = TableFormatter::new(|_v| Some(true));
assert!(
requirement_matches_only_yanked(
&formatter,
&VersionReq::new("unresolved"),
&versions(&["1.0.0"]),
&yanked_versions(&["1.0.0"]),
)
.is_none()
);
}
#[test]
fn test_compile_requirement_none_is_false() {
let formatter = TableFormatter::new(|_v| Some(true));
assert!(
requirement_matches_only_yanked(
&formatter,
&VersionReq::new("not-modelled"),
&versions(&["1.0.0"]),
&yanked_versions(&["1.0.0"]),
)
.is_none()
);
}
#[test]
fn test_generate_diagnostics_from_cache_yanked_only_match_fires_yanked_diagnostic() {
let formatter = TableFormatter::new(|v| Some(v == "1.2.1"));
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "modelled".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into(), "1.2.1".into()]),
yanked: Arc::from(vec![("1.2.1".into(), RemovalStatus::Yanked)]),
published_at: None,
},
);
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(diagnostics.len(), 1, "expected exactly one diagnostic");
assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
assert_eq!(
diagnostics[0].message,
format!("{}; latest is 2.0.0", formatter.yanked_message())
);
}
#[test]
fn test_generate_diagnostics_from_cache_deprecation_never_suppresses_yanked_only_match_without_263_entry()
{
let formatter = TableFormatter::new(|v| Some(v == "1.2.1"));
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "modelled".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into(), "1.2.1".into()]),
yanked: Arc::from(vec![("1.2.1".into(), RemovalStatus::Yanked)]),
published_at: None,
},
);
let resolved_versions = HashMap::new();
let outcomes = DependencyOutcomes::new().with_deprecation(
"serde",
Deprecation {
reason: Some("archived".to_string()),
replacement: None,
},
);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(
diagnostics.len(),
2,
"expected exactly the deprecation diagnostic plus the #247 yanked-only \
match, got: {diagnostics:?}"
);
assert_eq!(
diagnostics[0].message,
format!("{}: archived", formatter.deprecated_message()),
"a genuine Yanked #247 match must still fire, without a #263 entry: {diagnostics:?}"
);
assert_eq!(
diagnostics[0].code,
Some(NumberOrString::String(DEPRECATED_DIAGNOSTIC_CODE.into()))
);
assert_eq!(
diagnostics[1].message,
format!("{}; latest is 2.0.0", formatter.yanked_message())
);
assert_eq!(diagnostics[1].code, None);
}
#[test]
fn test_generate_diagnostics_from_cache_deprecation_suppresses_yanked_only_match_without_263_entry()
{
let formatter = TableFormatter::new(|v| Some(v == "1.2.1"));
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "modelled".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into(), "1.2.1".into()]),
yanked: Arc::from(vec![("1.2.1".into(), RemovalStatus::AdvisoryDeprecated)]),
published_at: None,
},
);
let resolved_versions = HashMap::new();
let outcomes = DependencyOutcomes::new().with_deprecation(
"serde",
Deprecation {
reason: Some("archived".to_string()),
replacement: None,
},
);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
diagnostics
.iter()
.all(|d| !d.message.starts_with(formatter.yanked_message())),
"an AdvisoryDeprecated #247 match must yield to the co-occurring deprecation \
finding, got: {diagnostics:?}"
);
assert!(
diagnostics
.iter()
.any(|d| d.message.starts_with(formatter.deprecated_message())),
"the deprecation finding must still fire: {diagnostics:?}"
);
}
#[test]
fn test_generate_diagnostics_from_cache_suppressed_263_entry_does_not_gate_off_yanked_247_match()
{
let formatter = TableFormatter::new(|v| Some(v == "1.2.1"));
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "modelled".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into(), "1.2.1".into()]),
yanked: Arc::from(vec![("1.2.1".into(), RemovalStatus::Yanked)]),
published_at: None,
},
);
let resolved_versions = HashMap::new();
let outcomes = DependencyOutcomes::new()
.with_yanked("serde", ("2.0.0".into(), RemovalStatus::AdvisoryDeprecated))
.with_deprecation(
"serde",
Deprecation {
reason: Some("archived".to_string()),
replacement: None,
},
);
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(
diagnostics.len(),
2,
"expected exactly the deprecation diagnostic plus the #247 yanked-only \
match — the D5-suppressed #263 in-use-version diagnostic (for the \
deprecated 2.0.0) must stay suppressed and contribute nothing, got: \
{diagnostics:?}"
);
assert_eq!(
diagnostics[0].message,
format!("{}: archived", formatter.deprecated_message()),
"the package-level deprecation finding must still fire: {diagnostics:?}"
);
assert_eq!(
diagnostics[0].code,
Some(NumberOrString::String(DEPRECATED_DIAGNOSTIC_CODE.into()))
);
assert_eq!(
diagnostics[1].message,
format!("{}; latest is 2.0.0", formatter.yanked_message()),
"the #247 match against the genuinely Yanked 1.2.1 must still fire even though \
the unrelated #263 in-use-version finding was D5-suppressed, got: {diagnostics:?}"
);
assert_eq!(diagnostics[1].code, None);
}
#[test]
fn test_generate_diagnostics_from_cache_yanked_dedup_in_use_and_requirement_only_match() {
let formatter = TableFormatter::new(|v| Some(v == "1.2.1"));
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "modelled".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into(), "1.2.1".into()]),
yanked: Arc::from(vec![("1.2.1".into(), RemovalStatus::Yanked)]),
published_at: None,
},
);
let resolved_versions = HashMap::new();
let outcomes = DependencyOutcomes::new()
.with_yanked("serde", ("1.2.1".into(), RemovalStatus::Yanked));
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert_eq!(
diagnostics.len(),
2,
"expected exactly the yanked diagnostic plus the co-emitted outdated \
diagnostic (#263's policy), got: {diagnostics:?}"
);
assert_eq!(
diagnostics[0].message,
format!("{} (1.2.1)", formatter.yanked_message()),
"expected the in-use-version check (#263) to run first and win, got: {diagnostics:?}"
);
assert_eq!(diagnostics[0].code, None);
assert_eq!(
diagnostics[1].message, "Newer version available: 2.0.0",
"expected the co-emitted outdated diagnostic, got: {diagnostics:?}"
);
assert_eq!(diagnostics[1].code, None);
}
#[test]
fn test_generate_diagnostics_from_cache_yanked_only_match_uses_configured_severity() {
let formatter = TableFormatter::new(|v| Some(v == "1.2.1"));
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "modelled".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "1.2.1".into(),
available: Arc::from(vec!["1.2.1".into()]),
yanked: Arc::from(vec![("1.2.1".into(), RemovalStatus::Yanked)]),
published_at: None,
},
);
let resolved_versions = HashMap::new();
let severities = DiagnosticSeverities {
yanked: DiagnosticSeverity::ERROR,
..DiagnosticSeverities::default()
};
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
severities,
PublishTime::now(),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::ERROR));
}
#[test]
fn test_generate_diagnostics_from_cache_match_with_non_yanked_alternative_skips_yanked_diagnostic()
{
let formatter = TableFormatter::new(|v| Some(v == "1.0.0" || v == "1.0.1"));
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "modelled".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into(), "1.0.1".into(), "1.0.0".into()]),
yanked: Arc::from(vec![("1.0.0".into(), RemovalStatus::Yanked)]),
published_at: None,
},
);
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
!diagnostics
.iter()
.any(|d| d.message.starts_with(formatter.yanked_message())),
"a non-yanked match exists, so no yanked diagnostic should fire, got: {diagnostics:?}"
);
}
#[test]
fn test_undecided_candidate_prevents_true_verdict() {
let formatter = TableFormatter::new(|v| match v {
"1.2.1" => Some(true),
"unparseable" => None,
_ => Some(false),
});
assert!(
requirement_matches_only_yanked(
&formatter,
&VersionReq::new("modelled"),
&versions(&["1.2.1", "unparseable"]),
&yanked_versions(&["1.2.1"]),
)
.is_none()
);
}
#[test]
fn test_undecided_candidate_before_match_still_prevents_true_verdict() {
let formatter = TableFormatter::new(|v| match v {
"1.2.1" => Some(true),
"unparseable" => None,
_ => Some(false),
});
assert!(
requirement_matches_only_yanked(
&formatter,
&VersionReq::new("modelled"),
&versions(&["unparseable", "1.2.1"]),
&yanked_versions(&["1.2.1"]),
)
.is_none()
);
}
#[test]
fn test_oversized_requirement_is_false_without_compiling() {
let formatter =
TableFormatter::new(|_v| panic!("must not compile/scan an oversized requirement"));
let oversized = "1".repeat(MAX_REQUIREMENT_LEN + 1);
assert!(
requirement_matches_only_yanked(
&formatter,
&VersionReq::new(oversized),
&versions(&["1.0.0"]),
&yanked_versions(&["1.0.0"]),
)
.is_none()
);
}
#[test]
fn test_yanked_only_match_skipped_for_non_registry_sources() {
let formatter = TableFormatter::new(|v| Some(v == "1.2.1"));
let uri = crate::test_util::test_uri("/test/Cargo.toml");
let mut cached_versions = HashMap::new();
cached_versions.insert(
"dep".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into(), "1.2.1".into()]),
yanked: Arc::from(vec![("1.2.1".into(), RemovalStatus::Yanked)]),
published_at: None,
},
);
let resolved_versions = HashMap::new();
let dep = MockDep {
name: "dep".into(),
version_req: "modelled".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 3)),
};
let parse_result = SingleDepParseResult {
dep: NonRegistryDep(
dep,
crate::parser::DependencySource::Path {
path: "../local".into(),
},
),
uri,
};
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
diagnostics
.iter()
.all(|d| !d.message.starts_with(formatter.yanked_message())),
"a path dependency must never produce the yanked diagnostic, got: {diagnostics:?}"
);
}
#[test]
fn test_yanked_only_match_suppresses_outdated_diagnostic() {
let formatter = TableFormatter::new(|v| Some(v == "1.2.1"));
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "modelled".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into(), "1.2.1".into()]),
yanked: Arc::from(vec![("1.2.1".into(), RemovalStatus::Yanked)]),
published_at: None,
},
);
let resolved_versions = HashMap::new();
let diagnostics = generate_diagnostics_from_cache(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
&formatter,
parse_result.uri(),
crate::freshness::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
assert!(
!diagnostics
.iter()
.any(|d| d.message.contains("Newer version available")),
"the yanked diagnostic must suppress the outdated hint, not add to it, got: {diagnostics:?}"
);
}
}
}