use crate::package::strip_build_metadata;
use crate::position::Position;
use super::{
BoundedVersionReq, RequirementMatcher, RequirementStatus, is_same_major_minor,
position_in_range,
};
use crate::{
ConcreteVersion, Dependency, EcosystemId, InvalidPackageName, PackageName, VersionReq,
};
pub trait PackageNaming: Send + Sync {
fn normalize_package_name(&self, name: &PackageName) -> String {
name.as_str().to_string()
}
fn validate_package_name(&self, _name: &str) -> Result<(), InvalidPackageName> {
Ok(())
}
}
pub trait PackageRendering: Send + Sync {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String;
fn format_version_replacing(&self, version: &ConcreteVersion, _current: &str) -> String {
self.format_version_for_text_edit(version)
}
fn format_version_replacing_for(
&self,
_dep: &dyn Dependency,
version: &ConcreteVersion,
current: &str,
) -> String {
self.format_version_replacing(version, current)
}
fn format_version_for_completion(
&self,
version: &ConcreteVersion,
typed_prefix: &str,
) -> String {
let _ = typed_prefix;
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String;
fn suppress_package_url(&self, source: &crate::parser::DependencySource) -> bool {
!matches!(source, crate::parser::DependencySource::Registry)
}
fn is_position_on_dependency(&self, dep: &dyn Dependency, position: Position) -> bool {
dep.version_range()
.is_some_and(|r| position_in_range(position, r))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BareMeaning {
Caret,
ExactPin,
Floor,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildMetadataPolicy {
Ignored,
Significant,
}
impl BuildMetadataPolicy {
#[must_use]
pub fn versions_equal(self, a: &str, b: &str) -> bool {
match self {
Self::Ignored => strip_build_metadata(a) == strip_build_metadata(b),
Self::Significant => a == b,
}
}
}
#[must_use]
pub const fn bare_meaning(ecosystem: EcosystemId) -> BareMeaning {
match ecosystem {
EcosystemId::Cargo => BareMeaning::Caret,
EcosystemId::Maven | EcosystemId::Gradle | EcosystemId::NuGet => BareMeaning::Floor,
EcosystemId::Npm
| EcosystemId::Pypi
| EcosystemId::Go
| EcosystemId::Bundler
| EcosystemId::Dart
| EcosystemId::Composer
| EcosystemId::Swift
| EcosystemId::Deno
| EcosystemId::GithubActions
| EcosystemId::GitlabCi => BareMeaning::ExactPin,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequirementRewriteShape {
Bare,
ExplicitCaret,
ExactPin,
BracketExactPin,
Tilde,
AnyVersion,
PartialWildcard,
SingleBound,
Compound,
}
const REWRITE_OPERATOR_PREFIXES: [&str; 9] = ["~>", "!=", "<=", ">=", "=", "~", "^", "<", ">"];
fn strip_requirement_operator(trimmed: &str) -> &str {
REWRITE_OPERATOR_PREFIXES
.iter()
.find_map(|op| trimmed.strip_prefix(op))
.map_or(trimmed, str::trim_start)
}
#[must_use]
pub fn requirement_is_compound(requirement: &str) -> bool {
let trimmed = requirement.trim();
trimmed.contains(',')
|| trimmed.contains("||")
|| strip_requirement_operator(trimmed).contains(char::is_whitespace)
}
fn requirement_is_any_version_wildcard(requirement: &str) -> bool {
crate::is_existence_wildcard_str(requirement) || requirement.eq_ignore_ascii_case("any")
}
fn requirement_is_partial_wildcard_shape(requirement: &str) -> bool {
if requirement.len() > 1 && requirement.ends_with('+') {
return true;
}
if requirement.contains('*') {
return true;
}
let core = requirement.split(['-', '+']).next().unwrap_or(requirement);
core.split('.').any(|segment| matches!(segment, "x" | "X"))
}
fn requirement_is_bracket_exact_pin(requirement: &str) -> bool {
requirement.len() > 2
&& requirement.starts_with('[')
&& requirement.ends_with(']')
&& !requirement.contains(',')
}
#[must_use]
pub fn classify_requirement_rewrite_shape(requirement: &str) -> RequirementRewriteShape {
let trimmed = requirement.trim();
if requirement_is_compound(trimmed) {
return RequirementRewriteShape::Compound;
}
if trimmed.starts_with('=') {
return RequirementRewriteShape::ExactPin;
}
if requirement_is_bracket_exact_pin(trimmed) {
return RequirementRewriteShape::BracketExactPin;
}
if trimmed.starts_with("~>") || trimmed.starts_with('~') {
return RequirementRewriteShape::Tilde;
}
if trimmed.starts_with('^') {
return RequirementRewriteShape::ExplicitCaret;
}
if requirement_is_any_version_wildcard(trimmed) {
return RequirementRewriteShape::AnyVersion;
}
if requirement_is_partial_wildcard_shape(trimmed) {
return RequirementRewriteShape::PartialWildcard;
}
if ["<=", "<", ">=", ">"]
.iter()
.any(|op| trimmed.starts_with(op))
{
return RequirementRewriteShape::SingleBound;
}
RequirementRewriteShape::Bare
}
#[must_use]
pub fn format_version_replacing_by_shape(
version: &ConcreteVersion,
current: &str,
bare_meaning: BareMeaning,
bare: impl FnOnce() -> String,
) -> String {
match classify_requirement_rewrite_shape(current) {
RequirementRewriteShape::Compound
| RequirementRewriteShape::PartialWildcard
| RequirementRewriteShape::SingleBound => match bare_meaning {
BareMeaning::Caret | BareMeaning::Floor => current.to_string(),
BareMeaning::ExactPin => bare(),
},
RequirementRewriteShape::ExplicitCaret => match bare_meaning {
BareMeaning::Caret => bare(),
BareMeaning::Floor => current.to_string(),
BareMeaning::ExactPin => format!("^{}", version.as_str()),
},
RequirementRewriteShape::AnyVersion | RequirementRewriteShape::Bare => bare(),
RequirementRewriteShape::ExactPin => format!("={}", version.as_str()),
RequirementRewriteShape::BracketExactPin => format!("[{}]", version.as_str()),
RequirementRewriteShape::Tilde => {
let prefix = if current.trim_start().starts_with("~>") {
"~>"
} else {
"~"
};
format!("{prefix}{}", version.as_str())
}
}
}
fn split_patch_component(component: &str) -> (&str, bool) {
component
.char_indices()
.find(|&(_, c)| c == '-' || c == '+')
.map_or((component, false), |(idx, marker)| {
(component.split_at(idx).0, marker == '-')
})
}
fn tilde_admits_version(req: &str, version: &str) -> bool {
if !is_same_major_minor(req, version) {
return false;
}
let Some(required_component) = req.split('.').nth(2) else {
return true;
};
let (required_core, required_is_prerelease) = split_patch_component(required_component);
let Ok(required_patch) = required_core.parse::<u64>() else {
return true;
};
let candidate_component = version.split('.').nth(2).unwrap_or("0");
let (candidate_core, candidate_is_prerelease) = split_patch_component(candidate_component);
let Ok(candidate_patch) = candidate_core.parse::<u64>() else {
return true;
};
match candidate_patch.cmp(&required_patch) {
std::cmp::Ordering::Less => false,
std::cmp::Ordering::Greater => true,
std::cmp::Ordering::Equal => !candidate_is_prerelease || required_is_prerelease,
}
}
fn parse_caret_components(parts: &[&str]) -> Option<[u64; 3]> {
let mut out = [0u64; 3];
for (slot, part) in out.iter_mut().zip(parts.iter()) {
*slot = part.parse().ok()?;
}
Some(out)
}
fn is_wildcard_component(part: &str) -> bool {
matches!(part, "x" | "X" | "*")
}
fn truncate_at_wildcard<'a>(req_parts: &'a [&'a str]) -> &'a [&'a str] {
let Some(idx) = req_parts
.iter()
.position(|part| is_wildcard_component(part))
else {
return req_parts;
};
req_parts.get(..idx).unwrap_or(req_parts)
}
fn matches_wildcard_components(req_parts: &[&str], version_parts: &[&str]) -> bool {
if !req_parts.iter().any(|part| is_wildcard_component(part)) {
return false;
}
let effective = truncate_at_wildcard(req_parts);
version_parts.len() >= effective.len()
&& effective
.iter()
.zip(version_parts.iter())
.all(|(req, ver)| req == ver)
}
fn caret_upper_bound(lower: [u64; 3], req_parts: &[&str]) -> Option<[u64; 3]> {
let given_len = req_parts.len().min(3);
let bump_index = lower
.iter()
.take(given_len)
.position(|&component| component != 0)
.unwrap_or_else(|| given_len.saturating_sub(1));
let [major, minor, patch] = lower;
Some(match bump_index {
0 => [major.checked_add(1)?, 0, 0],
1 => [major, minor.checked_add(1)?, 0],
_ => [major, minor, patch.checked_add(1)?],
})
}
#[expect(
clippy::string_slice,
reason = "cut is either a `find(['-', '+'])` match index (both ASCII, so always a char \
boundary) or version.len() itself, so the slice bound is always a char boundary"
)]
fn strip_version_suffix(version: &str) -> &str {
let cut = version.find(['-', '+']).unwrap_or(version.len());
&version[..cut]
}
fn relax_caret_floors(requirement: &str, bound_sep: &str) -> Option<String> {
let is_sep = |c: char| c.is_whitespace() || matches!(c, '|' | ',');
let mut out = String::with_capacity(requirement.len());
let mut replaced = false;
let mut rest = requirement;
while !rest.is_empty() {
let (token, tail) = rest.split_at(rest.find(is_sep).unwrap_or(rest.len()));
let (sep, next) = tail.split_at(tail.find(|c: char| !is_sep(c)).unwrap_or(tail.len()));
let bound = token.strip_prefix('^').and_then(|body| {
let parts: Vec<&str> = strip_version_suffix(body).split('.').collect();
let effective = truncate_at_wildcard(&parts);
if effective.is_empty() {
return None;
}
let lower = parse_caret_components(effective)?;
Some((lower[0], caret_upper_bound(lower, effective)?))
});
match bound {
Some((floor_major, [major, minor, patch])) => {
replaced = true;
out.push_str(&format!(
">={floor_major}.0.0{bound_sep}<{major}.{minor}.{patch}"
));
}
None => out.push_str(token),
}
out.push_str(sep);
rest = next;
}
replaced.then_some(out)
}
fn caret_admits_up_to_date(latest: &str, requirement: &str) -> Option<bool> {
let req = requirement.strip_prefix('^')?;
if req.contains(|c: char| {
c.is_whitespace() || matches!(c, '|' | '<' | '>' | '=' | '~' | '^' | ',')
}) {
return None;
}
let req = strip_version_suffix(req);
let req_parts: Vec<&str> = req.split('.').collect();
let ver_parts: Vec<&str> = strip_version_suffix(latest).split('.').collect();
match (req_parts.first(), ver_parts.first()) {
(Some(r), Some(v)) if is_wildcard_component(r) || r == v => {}
_ => return Some(false),
}
let effective_req_parts = truncate_at_wildcard(&req_parts);
if effective_req_parts.is_empty() {
return Some(true);
}
let (Some(lower), Some(candidate)) = (
parse_caret_components(effective_req_parts),
parse_caret_components(&ver_parts),
) else {
return Some(true);
};
Some(caret_upper_bound(lower, effective_req_parts).is_none_or(|upper| candidate < upper))
}
fn up_to_date_via_heuristic<F: RequirementResolution + ?Sized>(
formatter: &F,
requirement: BoundedVersionReq<'_>,
latest: &ConcreteVersion,
) -> bool {
caret_admits_up_to_date(latest.as_str(), requirement.as_str()).unwrap_or_else(|| {
formatter.version_satisfies_bounded_requirement(latest, requirement)
|| relax_caret_floors(requirement.as_str(), " ").is_some_and(|relaxed| {
let relaxed = VersionReq::new(relaxed);
BoundedVersionReq::new(&relaxed).is_some_and(|relaxed| {
formatter.version_satisfies_bounded_requirement(latest, relaxed)
})
})
})
}
fn requirement_needs_range_semantics(requirement: &str) -> bool {
match classify_requirement_rewrite_shape(requirement) {
RequirementRewriteShape::Compound
| RequirementRewriteShape::ExactPin
| RequirementRewriteShape::SingleBound => true,
RequirementRewriteShape::PartialWildcard => {
let trimmed = requirement.trim();
strip_requirement_operator(trimmed) != trimmed
}
RequirementRewriteShape::Bare
| RequirementRewriteShape::ExplicitCaret
| RequirementRewriteShape::BracketExactPin
| RequirementRewriteShape::Tilde
| RequirementRewriteShape::AnyVersion => false,
}
}
pub fn up_to_date_for_comparators_via_compiled_matcher<F: RequirementResolution + ?Sized>(
formatter: &F,
requirement: BoundedVersionReq<'_>,
latest: &ConcreteVersion,
) -> bool {
if requirement_needs_range_semantics(requirement.as_str()) {
up_to_date_via_compiled_matcher(formatter, requirement, latest)
} else {
up_to_date_via_heuristic(formatter, requirement, latest)
}
}
pub fn up_to_date_via_compiled_matcher<F: RequirementResolution + ?Sized>(
formatter: &F,
requirement: BoundedVersionReq<'_>,
latest: &ConcreteVersion,
) -> bool {
if let Some(admitted) = caret_admits_up_to_date(latest.as_str(), requirement.as_str()) {
return admitted;
}
let Some(matcher) = formatter.compile_bounded_requirement(requirement) else {
return formatter.version_satisfies_bounded_requirement(latest, requirement);
};
let core = ConcreteVersion::new(strip_version_suffix(latest.as_str()));
let admits = |m: &dyn RequirementMatcher| {
m.matches(latest) == Some(true)
|| (m.strict_prerelease_exclusion() && m.matches(&core) == Some(true))
};
let bound_sep = if requirement.as_str().contains(',') {
", "
} else {
" "
};
let relaxed = relax_caret_floors(requirement.as_str(), bound_sep)
.map(VersionReq::new)
.and_then(|relaxed| {
BoundedVersionReq::new(&relaxed)
.and_then(|bounded| formatter.compile_bounded_requirement(bounded))
});
if admits(&*matcher) || relaxed.as_deref().is_some_and(admits) {
true
} else if matcher.matches(latest).is_none() {
formatter.version_satisfies_bounded_requirement(latest, requirement)
} else {
false
}
}
pub trait RequirementResolution: Send + Sync {
fn build_metadata_policy(&self) -> BuildMetadataPolicy {
BuildMetadataPolicy::Ignored
}
fn version_satisfies_bounded_requirement(
&self,
version: &ConcreteVersion,
requirement: BoundedVersionReq<'_>,
) -> bool {
let requirement = requirement.as_str();
let version = version.as_str();
if let Some(req) = requirement.strip_prefix('^') {
let req = strip_version_suffix(req);
let req_parts: Vec<&str> = req.split('.').collect();
let ver_parts: Vec<&str> = strip_version_suffix(version).split('.').collect();
match (req_parts.first(), ver_parts.first()) {
(Some(r), Some(v)) if is_wildcard_component(r) || r == v => {}
_ => return false,
}
let effective_req_parts = truncate_at_wildcard(&req_parts);
if effective_req_parts.is_empty() {
return true;
}
let (Some(lower), Some(candidate)) = (
parse_caret_components(effective_req_parts),
parse_caret_components(&ver_parts),
) else {
return true;
};
return candidate >= lower
&& caret_upper_bound(lower, effective_req_parts)
.is_none_or(|upper| candidate < upper);
}
if let Some(req) = requirement.strip_prefix('~') {
return tilde_admits_version(req, version);
}
let req_parts: Vec<&str> = requirement.split('.').collect();
let is_partial_version = req_parts.len() <= 2;
let version_core = split_patch_component(version).0;
let ver_parts: Vec<&str> = version_core.split('.').collect();
self.build_metadata_policy().versions_equal(
version,
requirement.strip_prefix('=').unwrap_or(requirement),
) || (is_partial_version && is_same_major_minor(requirement, version_core))
|| matches_wildcard_components(&req_parts, &ver_parts)
}
fn is_bounded_requirement_up_to_date(
&self,
requirement: BoundedVersionReq<'_>,
latest: &ConcreteVersion,
) -> bool {
up_to_date_via_heuristic(self, requirement, latest)
}
fn bounded_requirement_is_unresolved(&self, requirement: BoundedVersionReq<'_>) -> bool {
self.bounded_requirement_is_placeholder(requirement)
}
fn bounded_requirement_is_placeholder(&self, requirement: BoundedVersionReq<'_>) -> bool {
super::requirement_contains_template_placeholder(requirement.as_str())
}
fn classify_requirement_status(
&self,
requirement: BoundedVersionReq<'_>,
latest: &ConcreteVersion,
) -> RequirementStatus {
if self.bounded_requirement_is_unresolved(requirement) {
return RequirementStatus::Unresolved;
}
if self.is_bounded_requirement_up_to_date(requirement, latest) {
RequirementStatus::UpToDate
} else {
RequirementStatus::Outdated
}
}
fn classify_requirement_status_for(
&self,
dep: &dyn Dependency,
requirement: BoundedVersionReq<'_>,
latest: &ConcreteVersion,
) -> RequirementStatus {
let _ = dep;
self.classify_requirement_status(requirement, latest)
}
fn compile_bounded_requirement(
&self,
_requirement: BoundedVersionReq<'_>,
) -> Option<Box<dyn RequirementMatcher>> {
None
}
fn bounded_requirement_already_resolves_to(
&self,
requirement: BoundedVersionReq<'_>,
target: &ConcreteVersion,
) -> bool {
self.compile_bounded_requirement(requirement)
.is_some_and(|matcher| matcher.matches(target) == Some(true))
}
fn bounded_requirement_is_undecidable_given_available(
&self,
_requirement: BoundedVersionReq<'_>,
_available: &[ConcreteVersion],
) -> bool {
false
}
fn manifest_requirement_is_resolved_version(&self, dep: &dyn Dependency) -> bool {
let _ = dep;
false
}
fn resolved_pin_version(
&self,
dep: &dyn Dependency,
) -> Option<crate::lsp_helpers::ResolvedPin> {
let _ = dep;
None
}
fn resolved_pin_version_depends_on_registry_fetch(&self) -> bool {
false
}
}
pub trait RequirementGate: RequirementResolution {
fn requirement_is_unresolved(&self, requirement: &VersionReq) -> bool;
fn requirement_is_placeholder(&self, requirement: &VersionReq) -> bool;
fn requirement_is_undecidable_given_available(
&self,
requirement: &VersionReq,
available: &[ConcreteVersion],
) -> bool;
fn version_satisfies_requirement(
&self,
version: &ConcreteVersion,
requirement: &VersionReq,
) -> bool;
fn is_requirement_up_to_date(&self, requirement: &VersionReq, latest: &ConcreteVersion)
-> bool;
fn compile_requirement(&self, requirement: &VersionReq) -> Option<Box<dyn RequirementMatcher>>;
fn requirement_already_resolves_to(
&self,
requirement: &VersionReq,
target: &ConcreteVersion,
) -> bool;
fn requirement_status(
&self,
requirement: &VersionReq,
latest: &ConcreteVersion,
) -> RequirementStatus;
fn requirement_status_for(
&self,
dep: &dyn Dependency,
requirement: &VersionReq,
latest: &ConcreteVersion,
) -> RequirementStatus;
}
impl<T: RequirementResolution + ?Sized> RequirementGate for T {
fn requirement_is_unresolved(&self, requirement: &VersionReq) -> bool {
BoundedVersionReq::new(requirement)
.is_none_or(|requirement| self.bounded_requirement_is_unresolved(requirement))
}
fn requirement_is_placeholder(&self, requirement: &VersionReq) -> bool {
BoundedVersionReq::new(requirement)
.is_none_or(|requirement| self.bounded_requirement_is_placeholder(requirement))
}
fn requirement_is_undecidable_given_available(
&self,
requirement: &VersionReq,
available: &[ConcreteVersion],
) -> bool {
BoundedVersionReq::new(requirement).is_none_or(|requirement| {
self.bounded_requirement_is_undecidable_given_available(requirement, available)
})
}
fn version_satisfies_requirement(
&self,
version: &ConcreteVersion,
requirement: &VersionReq,
) -> bool {
BoundedVersionReq::new(requirement).is_some_and(|requirement| {
self.version_satisfies_bounded_requirement(version, requirement)
})
}
fn is_requirement_up_to_date(
&self,
requirement: &VersionReq,
latest: &ConcreteVersion,
) -> bool {
BoundedVersionReq::new(requirement)
.is_none_or(|requirement| self.is_bounded_requirement_up_to_date(requirement, latest))
}
fn compile_requirement(&self, requirement: &VersionReq) -> Option<Box<dyn RequirementMatcher>> {
BoundedVersionReq::new(requirement)
.and_then(|requirement| self.compile_bounded_requirement(requirement))
}
fn requirement_already_resolves_to(
&self,
requirement: &VersionReq,
target: &ConcreteVersion,
) -> bool {
BoundedVersionReq::new(requirement).is_some_and(|requirement| {
self.bounded_requirement_already_resolves_to(requirement, target)
})
}
fn requirement_status(
&self,
requirement: &VersionReq,
latest: &ConcreteVersion,
) -> RequirementStatus {
BoundedVersionReq::new(requirement).map_or(RequirementStatus::Unresolved, |requirement| {
self.classify_requirement_status(requirement, latest)
})
}
fn requirement_status_for(
&self,
dep: &dyn Dependency,
requirement: &VersionReq,
latest: &ConcreteVersion,
) -> RequirementStatus {
BoundedVersionReq::new(requirement).map_or(RequirementStatus::Unresolved, |requirement| {
self.classify_requirement_status_for(dep, requirement, latest)
})
}
}
pub trait DiagnosticMessages: Send + Sync {
fn yanked_message(&self) -> &'static str {
"This version has been yanked"
}
fn yanked_label(&self) -> &'static str {
"*(yanked)*"
}
fn deprecated_message(&self) -> &'static str {
"This package is deprecated"
}
fn sibling_match_label(&self) -> &'static str {
"matched tag"
}
fn deprecated_label(&self) -> &'static str {
"*(deprecated)*"
}
}
pub trait DiagnosticPolicy: Send + Sync {
fn supports_package_rename(&self) -> bool {
false
}
fn yanked_diagnostic_applies_to(
&self,
_dep: &dyn Dependency,
_requirement: &VersionReq,
) -> bool {
true
}
}
pub trait SourcePolicy: Send + Sync {
fn can_resolve_source(&self, source: &crate::parser::DependencySource) -> bool {
source.is_version_resolvable()
|| (self.resolves_alternate_registry()
&& matches!(
source,
crate::parser::DependencySource::AlternateRegistry { .. }
))
}
fn resolves_alternate_registry(&self) -> bool {
false
}
fn source_is_public_registry_content(&self, source: &crate::parser::DependencySource) -> bool {
matches!(source, crate::parser::DependencySource::Registry)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OsvNameAvailability {
Ready,
AwaitingRegistryData {
written_fallback: Option<crate::osv::OsvPackageName>,
},
}
pub trait OsvNaming: Send + Sync {
fn osv_package_name(&self, dep: &dyn Dependency) -> Option<crate::osv::OsvPackageName> {
crate::osv::OsvPackageName::new_or_skip(dep.name().as_str())
}
fn osv_name_availability(&self, dep: &dyn Dependency) -> OsvNameAvailability {
let _ = dep;
OsvNameAvailability::Ready
}
fn osv_version_to_native(&self, version: &crate::osv::OsvVersion) -> ConcreteVersion {
ConcreteVersion::new(version.as_str())
}
fn osv_version(&self, version: &ConcreteVersion) -> crate::osv::OsvVersion {
crate::osv::OsvVersion::new(crate::github::normalize_tag(version.as_str()))
}
}
pub trait EcosystemFormatter:
PackageNaming
+ PackageRendering
+ RequirementResolution
+ DiagnosticMessages
+ DiagnosticPolicy
+ SourcePolicy
+ OsvNaming
{
}
impl<
T: PackageNaming
+ PackageRendering
+ RequirementResolution
+ DiagnosticMessages
+ DiagnosticPolicy
+ SourcePolicy
+ OsvNaming,
> EcosystemFormatter for T
{
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lsp_helpers::test_support::MOCK_FORMATTER;
#[test]
fn test_requirement_status_oversized_requirement_is_unresolved() {
let oversized = VersionReq::new("1".repeat(300));
let latest = ConcreteVersion::new("1.0.0");
assert_eq!(
MOCK_FORMATTER.requirement_status(&oversized, &latest),
RequirementStatus::Unresolved
);
}
#[test]
fn test_requirement_status_ordinary_requirement_unaffected() {
let requirement = VersionReq::new("1.0.0");
let latest = ConcreteVersion::new("1.0.0");
assert_eq!(
MOCK_FORMATTER.requirement_status(&requirement, &latest),
RequirementStatus::UpToDate
);
}
#[test]
fn test_requirement_status_for_oversized_requirement_bypasses_misbehaving_override() {
struct AlwaysOutdatedFormatter;
impl PackageNaming for AlwaysOutdatedFormatter {}
impl PackageRendering for AlwaysOutdatedFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
name.as_str().to_string()
}
}
impl RequirementResolution for AlwaysOutdatedFormatter {
fn classify_requirement_status_for(
&self,
_dep: &dyn Dependency,
_requirement: BoundedVersionReq<'_>,
_latest: &ConcreteVersion,
) -> RequirementStatus {
RequirementStatus::Outdated
}
}
impl DiagnosticMessages for AlwaysOutdatedFormatter {}
impl DiagnosticPolicy for AlwaysOutdatedFormatter {}
impl SourcePolicy for AlwaysOutdatedFormatter {}
impl OsvNaming for AlwaysOutdatedFormatter {}
let oversized = VersionReq::new("1".repeat(300));
let within_cap = VersionReq::new("^1.0");
let latest = ConcreteVersion::new("1.0.0");
let range = crate::position::Range::new(
crate::position::Position::new(0, 0),
crate::position::Position::new(0, 1),
);
let dep = |requirement: &VersionReq| crate::lsp_helpers::test_support::MockDep {
name: PackageName::new("pkg"),
version_req: requirement.clone(),
version_range: range,
name_range: range,
};
assert_eq!(
AlwaysOutdatedFormatter.requirement_status_for(&dep(&within_cap), &within_cap, &latest),
RequirementStatus::Outdated
);
assert_eq!(
AlwaysOutdatedFormatter.requirement_status_for(&dep(&oversized), &oversized, &latest),
RequirementStatus::Unresolved
);
}
#[test]
fn test_oversized_requirement_bypasses_misbehaving_hook_overrides() {
struct AcceptAllMatcher;
impl RequirementMatcher for AcceptAllMatcher {
fn matches(&self, _version: &ConcreteVersion) -> Option<bool> {
Some(true)
}
fn strict_prerelease_exclusion(&self) -> bool {
false
}
}
struct MisbehavingFormatter;
impl PackageNaming for MisbehavingFormatter {}
impl PackageRendering for MisbehavingFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
name.as_str().to_string()
}
}
impl RequirementResolution for MisbehavingFormatter {
fn compile_bounded_requirement(
&self,
_requirement: BoundedVersionReq<'_>,
) -> Option<Box<dyn RequirementMatcher>> {
Some(Box::new(AcceptAllMatcher))
}
fn is_bounded_requirement_up_to_date(
&self,
_requirement: BoundedVersionReq<'_>,
_latest: &ConcreteVersion,
) -> bool {
false
}
fn bounded_requirement_already_resolves_to(
&self,
_requirement: BoundedVersionReq<'_>,
_target: &ConcreteVersion,
) -> bool {
true
}
}
impl DiagnosticMessages for MisbehavingFormatter {}
impl DiagnosticPolicy for MisbehavingFormatter {}
impl SourcePolicy for MisbehavingFormatter {}
impl OsvNaming for MisbehavingFormatter {}
let oversized = VersionReq::new("1".repeat(crate::lsp_helpers::MAX_REQUIREMENT_LEN + 1));
let at_cap = VersionReq::new("1".repeat(crate::lsp_helpers::MAX_REQUIREMENT_LEN));
let within_cap = VersionReq::new("^1.0");
let latest = ConcreteVersion::new("1.0.0");
for requirement in [&within_cap, &at_cap] {
assert!(
MisbehavingFormatter
.compile_requirement(requirement)
.is_some()
);
assert!(!MisbehavingFormatter.is_requirement_up_to_date(requirement, &latest));
assert!(MisbehavingFormatter.requirement_already_resolves_to(requirement, &latest));
}
assert!(
MisbehavingFormatter
.compile_requirement(&oversized)
.is_none()
);
assert!(MisbehavingFormatter.is_requirement_up_to_date(&oversized, &latest));
assert!(!MisbehavingFormatter.requirement_already_resolves_to(&oversized, &latest));
let dyn_formatter: &dyn RequirementResolution = &MisbehavingFormatter;
assert!(dyn_formatter.compile_requirement(&within_cap).is_some());
assert!(!dyn_formatter.is_requirement_up_to_date(&within_cap, &latest));
assert!(dyn_formatter.requirement_already_resolves_to(&within_cap, &latest));
assert!(dyn_formatter.compile_requirement(&oversized).is_none());
assert!(dyn_formatter.is_requirement_up_to_date(&oversized, &latest));
assert!(!dyn_formatter.requirement_already_resolves_to(&oversized, &latest));
}
#[test]
fn test_oversized_requirement_bypasses_misbehaving_predicate_overrides() {
struct MisbehavingPredicates;
impl PackageNaming for MisbehavingPredicates {}
impl PackageRendering for MisbehavingPredicates {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
name.as_str().to_string()
}
}
impl RequirementResolution for MisbehavingPredicates {
fn bounded_requirement_is_unresolved(
&self,
_requirement: BoundedVersionReq<'_>,
) -> bool {
false
}
fn bounded_requirement_is_placeholder(
&self,
_requirement: BoundedVersionReq<'_>,
) -> bool {
false
}
fn bounded_requirement_is_undecidable_given_available(
&self,
_requirement: BoundedVersionReq<'_>,
_available: &[ConcreteVersion],
) -> bool {
false
}
fn version_satisfies_bounded_requirement(
&self,
_version: &ConcreteVersion,
_requirement: BoundedVersionReq<'_>,
) -> bool {
true
}
}
impl DiagnosticMessages for MisbehavingPredicates {}
impl DiagnosticPolicy for MisbehavingPredicates {}
impl SourcePolicy for MisbehavingPredicates {}
impl OsvNaming for MisbehavingPredicates {}
let oversized = VersionReq::new("1".repeat(crate::lsp_helpers::MAX_REQUIREMENT_LEN + 1));
let at_cap = VersionReq::new("1".repeat(crate::lsp_helpers::MAX_REQUIREMENT_LEN));
let within_cap = VersionReq::new("^1.0");
let version = ConcreteVersion::new("1.0.0");
let available = [ConcreteVersion::new("1.0.0")];
for requirement in [&within_cap, &at_cap] {
assert!(!MisbehavingPredicates.requirement_is_unresolved(requirement));
assert!(!MisbehavingPredicates.requirement_is_placeholder(requirement));
assert!(
!MisbehavingPredicates
.requirement_is_undecidable_given_available(requirement, &available)
);
assert!(MisbehavingPredicates.version_satisfies_requirement(&version, requirement));
}
assert!(MisbehavingPredicates.requirement_is_unresolved(&oversized));
assert!(MisbehavingPredicates.requirement_is_placeholder(&oversized));
assert!(
MisbehavingPredicates
.requirement_is_undecidable_given_available(&oversized, &available)
);
assert!(!MisbehavingPredicates.version_satisfies_requirement(&version, &oversized));
let dyn_formatter: &dyn RequirementResolution = &MisbehavingPredicates;
assert!(!dyn_formatter.requirement_is_placeholder(&within_cap));
assert!(dyn_formatter.requirement_is_placeholder(&oversized));
assert!(dyn_formatter.version_satisfies_requirement(&version, &within_cap));
assert!(!dyn_formatter.version_satisfies_requirement(&version, &oversized));
}
#[test]
fn test_version_satisfies_requirement_tilde_enforces_patch_floor() {
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.0"),
&VersionReq::new("~1.5.3")
));
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.3"),
&VersionReq::new("~1.5.3")
));
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.9"),
&VersionReq::new("~1.5.3")
));
}
#[test]
fn test_version_satisfies_requirement_tilde_without_patch_admits_any_patch() {
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.0"),
&VersionReq::new("~1.5")
));
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.9"),
&VersionReq::new("~1.5")
));
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.6.0"),
&VersionReq::new("~1.5")
));
}
#[test]
fn test_version_satisfies_requirement_tilde_suffixed_patch_still_enforces_floor() {
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.0-beta.1"),
&VersionReq::new("~1.5.3")
));
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.0"),
&VersionReq::new("~1.5.3-beta.1")
));
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.0"),
&VersionReq::new("~1.5.3+build")
));
}
#[test]
fn test_version_satisfies_requirement_tilde_prerelease_at_exact_patch_is_rejected() {
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.3-beta"),
&VersionReq::new("~1.5.3")
));
}
#[test]
fn test_version_satisfies_requirement_tilde_prerelease_requirement_admits_same_patch_prerelease()
{
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.3-beta.1"),
&VersionReq::new("~1.5.3-beta.1")
));
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.3-rc.1"),
&VersionReq::new("~1.5.3-beta.1")
));
}
#[test]
fn test_requirement_status_tilde_suffixed_candidate_is_outdated() {
let requirement = VersionReq::new("~1.5.3");
assert_eq!(
MOCK_FORMATTER.requirement_status(&requirement, &ConcreteVersion::new("1.5.0-beta.1")),
RequirementStatus::Outdated
);
}
#[test]
fn test_version_satisfies_requirement_tilde_build_metadata_at_exact_patch_is_admitted() {
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.3+build"),
&VersionReq::new("~1.5.3")
));
}
#[test]
fn test_version_satisfies_requirement_tilde_wildcard_patch_is_permissive() {
for requirement in ["~1.5.x", "~1.5.X", "~1.5.*"] {
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.0"),
&VersionReq::new(requirement)
));
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.9"),
&VersionReq::new(requirement)
));
}
}
#[test]
fn test_version_satisfies_requirement_tilde_missing_candidate_patch_treated_as_zero() {
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5"),
&VersionReq::new("~1.5.3")
));
}
#[test]
fn test_requirement_status_tilde_below_patch_floor_is_outdated() {
let requirement = VersionReq::new("~1.5.3");
assert_eq!(
MOCK_FORMATTER.requirement_status(&requirement, &ConcreteVersion::new("1.5.0")),
RequirementStatus::Outdated
);
assert_eq!(
MOCK_FORMATTER.requirement_status(&requirement, &ConcreteVersion::new("1.5.3")),
RequirementStatus::UpToDate
);
}
#[test]
fn test_version_satisfies_requirement_partial_version_rejects_string_prefix_match() {
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.20.0"),
&VersionReq::new("1.2")
));
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.2.5"),
&VersionReq::new("1.2")
));
}
#[test]
fn test_requirement_status_partial_version_string_prefix_is_outdated() {
let requirement = VersionReq::new("1.2");
assert_eq!(
MOCK_FORMATTER.requirement_status(&requirement, &ConcreteVersion::new("1.20.0")),
RequirementStatus::Outdated
);
assert_eq!(
MOCK_FORMATTER.requirement_status(&requirement, &ConcreteVersion::new("1.2.5")),
RequirementStatus::UpToDate
);
}
#[test]
fn test_caret_upper_bound_overflow_returns_none_instead_of_panicking() {
let max = u64::MAX.to_string();
assert_eq!(caret_upper_bound([u64::MAX, 0, 0], &[max.as_str()]), None);
assert_eq!(
caret_upper_bound([0, u64::MAX, 0], &["0", max.as_str()]),
None
);
assert_eq!(
caret_upper_bound([0, 0, u64::MAX], &["0", "0", max.as_str()]),
None
);
}
#[test]
fn test_version_satisfies_requirement_caret_overflow_treated_as_unbounded_above() {
let max = u64::MAX.to_string();
let requirement = format!("^{max}");
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new(format!("{max}.0.0")),
&VersionReq::new(&requirement)
));
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("0.0.0"),
&VersionReq::new(&requirement)
));
}
#[test]
fn test_requirement_status_caret_latest_below_lower_bound_floor_stays_up_to_date() {
let requirement = VersionReq::new("^1.5");
let latest = ConcreteVersion::new("1.4.9");
assert_eq!(
MOCK_FORMATTER.requirement_status(&requirement, &latest),
RequirementStatus::UpToDate
);
}
#[test]
fn test_requirement_status_caret_latest_below_major_is_outdated() {
let requirement = VersionReq::new("^2.0");
let latest = ConcreteVersion::new("1.9.0");
assert_eq!(
MOCK_FORMATTER.requirement_status(&requirement, &latest),
RequirementStatus::Outdated
);
}
#[test]
fn test_version_satisfies_requirement_caret_candidate_suffix_still_enforces_lower_bound() {
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.4.9-beta"),
&VersionReq::new("^1.5")
));
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.4.9+build"),
&VersionReq::new("^1.5")
));
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.0-beta"),
&VersionReq::new("^1.5")
));
}
#[test]
fn test_requirement_status_caret_suffixed_latest_at_upper_bound_is_outdated() {
let requirement = VersionReq::new("^0.5");
assert_eq!(
MOCK_FORMATTER.requirement_status(&requirement, &ConcreteVersion::new("0.5.5-beta")),
RequirementStatus::UpToDate
);
assert_eq!(
MOCK_FORMATTER.requirement_status(&requirement, &ConcreteVersion::new("0.6.0-beta")),
RequirementStatus::Outdated
);
}
#[test]
fn test_version_satisfies_requirement_wildcard_matches() {
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.3.0"),
&VersionReq::new("1.x")
));
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.2.0"),
&VersionReq::new("1.2.x")
));
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.2.5"),
&VersionReq::new("1.2.*")
));
}
#[test]
fn test_version_satisfies_requirement_wildcard_rejects_mismatched_component() {
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("2.0.0"),
&VersionReq::new("1.x")
));
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.3.0"),
&VersionReq::new("1.2.x")
));
}
#[test]
fn test_version_satisfies_requirement_caret_wildcard_bounds_correctly() {
for requirement in ["^1.5.x", "^1.5.*"] {
assert!(
!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.4.9"),
&VersionReq::new(requirement)
),
"{requirement} must reject a candidate below the effective lower bound"
);
assert!(
MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.0"),
&VersionReq::new(requirement)
),
"{requirement} must accept a candidate at the lower bound"
);
assert!(
MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.9.9"),
&VersionReq::new(requirement)
),
"{requirement} must accept a candidate within range"
);
assert!(
!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("2.0.0"),
&VersionReq::new(requirement)
),
"{requirement} must reject a candidate at/above the upper bound"
);
assert!(
!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("0.9.0"),
&VersionReq::new(requirement)
),
"{requirement} must reject a candidate with a lower major version"
);
}
}
#[test]
fn test_version_satisfies_requirement_caret_requirement_suffix_still_bounds() {
let requirement = "^1.5.0-beta.1";
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.4.9"),
&VersionReq::new(requirement)
));
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.5.0"),
&VersionReq::new(requirement)
));
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("2.0.0"),
&VersionReq::new(requirement)
));
}
#[test]
fn test_version_satisfies_requirement_caret_wildcard_major_matches_any_version() {
for requirement in ["^x", "^*", "^x.5.0"] {
assert!(
MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("2.3.4"),
&VersionReq::new(requirement)
),
"{requirement} must admit a high-major candidate"
);
assert!(
MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("0.5.0"),
&VersionReq::new(requirement)
),
"{requirement} must admit a zero-major candidate too"
);
}
}
#[test]
fn test_requirement_status_caret_wildcard_major_matches_any_version() {
let requirement = VersionReq::new("^*");
assert_eq!(
MOCK_FORMATTER.requirement_status(&requirement, &ConcreteVersion::new("3.0.0")),
RequirementStatus::UpToDate
);
}
#[test]
fn test_is_requirement_up_to_date_caret_wildcard_no_fail_open() {
let requirement = VersionReq::new("^1.5.x");
assert!(
!MOCK_FORMATTER.is_requirement_up_to_date(&requirement, &ConcreteVersion::new("2.0.0")),
"latest past the effective upper bound must be reported outdated, not fail-open admitted"
);
assert!(
MOCK_FORMATTER.is_requirement_up_to_date(&requirement, &ConcreteVersion::new("1.4.9")),
"latest below the caret's own lower-bound floor stays up to date (#1622 S2)"
);
}
#[test]
fn test_build_metadata_policy_defaults_to_ignored() {
assert_eq!(
MOCK_FORMATTER.build_metadata_policy(),
BuildMetadataPolicy::Ignored
);
}
#[test]
fn test_pin_build_metadata_significant_policy_distinguishes_revisions() {
struct BuildAware;
impl RequirementResolution for BuildAware {
fn build_metadata_policy(&self) -> BuildMetadataPolicy {
BuildMetadataPolicy::Significant
}
}
for (pin, latest, expected) in [
("1.2.3+1", "1.2.3+23", false),
("1.2.3+23", "1.2.3+23", true),
("=1.2.3+1", "1.2.3+23", false),
] {
assert_eq!(
BuildAware.is_requirement_up_to_date(
&VersionReq::new(pin),
&ConcreteVersion::new(latest)
),
expected,
"pin {pin} vs latest {latest}"
);
}
}
#[test]
fn test_is_requirement_up_to_date_pin_ignores_build_metadata() {
for (pin, latest, expected) in [
("1.2.3", "1.2.3+build.7", true),
("1.2.3+build.1", "1.2.3", true),
("1.2.3+build.1", "1.2.3+build.7", true),
("1.2.3", "1.2.4+build.7", false),
("1.2.3-beta.1", "1.2.3+build.7", false),
("=1.2.3", "1.2.3", true),
("=1.2.3", "1.2.3+build.7", true),
("=300.3.1+3.3.1", "300.3.1+3.3.2", true),
("=1.2.3", "1.2.4", false),
] {
assert_eq!(
MOCK_FORMATTER.is_requirement_up_to_date(
&VersionReq::new(pin),
&ConcreteVersion::new(latest)
),
expected,
"pin {pin} vs latest {latest}"
);
}
}
#[test]
fn test_requirement_status_caret_wildcard_latest_past_upper_bound_is_outdated() {
let requirement = VersionReq::new("^1.5.x");
let latest = ConcreteVersion::new("3.0.0");
assert_eq!(
MOCK_FORMATTER.requirement_status(&requirement, &latest),
RequirementStatus::Outdated
);
}
#[test]
fn test_version_satisfies_requirement_wildcard_mid_position_truncates_trailing_components() {
assert!(
MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.3.4"),
&VersionReq::new("1.x.5")
),
"the trailing `.5` after a wildcard must not be enforced"
);
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.3.5"),
&VersionReq::new("1.x.5")
));
}
#[test]
fn test_version_satisfies_requirement_wildcard_uppercase_x() {
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.2.0"),
&VersionReq::new("1.2.X")
));
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.6.0"),
&VersionReq::new("^1.5.X")
));
}
#[test]
fn test_version_satisfies_requirement_caret_zero_major_wildcard_bounds() {
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("0.5.0"),
&VersionReq::new("^0.x")
));
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("1.0.0"),
&VersionReq::new("^0.x")
));
assert!(MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("0.0.5"),
&VersionReq::new("^0.0.x")
));
assert!(!MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("0.1.0"),
&VersionReq::new("^0.0.x")
));
}
#[test]
fn test_version_satisfies_requirement_bare_wildcard_matches_any_version() {
for requirement in ["*", "x", "X"] {
assert!(
MOCK_FORMATTER.version_satisfies_requirement(
&ConcreteVersion::new("9.9.9"),
&VersionReq::new(requirement)
),
"{requirement} must match any candidate"
);
}
}
#[test]
fn test_classify_requirement_rewrite_shape_bare() {
assert_eq!(
classify_requirement_rewrite_shape("1.2.3"),
RequirementRewriteShape::Bare
);
}
#[test]
fn test_classify_requirement_rewrite_shape_explicit_caret() {
assert_eq!(
classify_requirement_rewrite_shape("^1.2.3"),
RequirementRewriteShape::ExplicitCaret
);
}
#[test]
fn test_classify_requirement_rewrite_shape_explicit_caret_with_space() {
assert_eq!(
classify_requirement_rewrite_shape("^ 1.2.3"),
RequirementRewriteShape::ExplicitCaret
);
}
#[test]
fn test_classify_requirement_rewrite_shape_exact_pin() {
assert_eq!(
classify_requirement_rewrite_shape("=1.2.3"),
RequirementRewriteShape::ExactPin
);
}
#[test]
fn test_classify_requirement_rewrite_shape_tilde_both_spellings() {
assert_eq!(
classify_requirement_rewrite_shape("~1.2.3"),
RequirementRewriteShape::Tilde
);
assert_eq!(
classify_requirement_rewrite_shape("~>1.2.3"),
RequirementRewriteShape::Tilde
);
}
#[test]
fn test_classify_requirement_rewrite_shape_any_version() {
for requirement in ["*", "", "any", "ANY"] {
assert_eq!(
classify_requirement_rewrite_shape(requirement),
RequirementRewriteShape::AnyVersion,
"expected {requirement:?} to classify as AnyVersion"
);
}
}
#[test]
fn test_classify_requirement_rewrite_shape_partial_wildcard() {
assert_eq!(
classify_requirement_rewrite_shape("1.2.*"),
RequirementRewriteShape::PartialWildcard
);
assert_eq!(
classify_requirement_rewrite_shape("1.x"),
RequirementRewriteShape::PartialWildcard
);
}
#[test]
fn test_classify_requirement_rewrite_shape_prerelease_x_not_wildcard() {
assert_eq!(
classify_requirement_rewrite_shape("1.0.0-alpha.x"),
RequirementRewriteShape::Bare
);
}
#[test]
fn test_classify_requirement_rewrite_shape_wildcard_in_prerelease_part() {
assert_eq!(
classify_requirement_rewrite_shape("1.2.0-rc.*"),
RequirementRewriteShape::PartialWildcard
);
}
#[test]
fn test_classify_requirement_rewrite_shape_build_metadata_x_not_wildcard() {
assert_eq!(
classify_requirement_rewrite_shape("1.2.3+build.x"),
RequirementRewriteShape::Bare
);
}
#[test]
fn test_format_version_replacing_by_shape_empty_requirement_always_collapses() {
let new_version = ConcreteVersion::new("2.0.0");
for bare_meaning in [
BareMeaning::Caret,
BareMeaning::ExactPin,
BareMeaning::Floor,
] {
assert_eq!(
format_version_replacing_by_shape(&new_version, "", bare_meaning, || {
new_version.to_string()
}),
"2.0.0"
);
}
}
#[test]
fn test_classify_requirement_rewrite_shape_single_bound_both_directions() {
for requirement in ["<1.5", "<=1.5.0", ">1.0", ">=1.2"] {
assert_eq!(
classify_requirement_rewrite_shape(requirement),
RequirementRewriteShape::SingleBound,
"expected {requirement:?} to classify as SingleBound"
);
}
}
#[test]
fn test_classify_requirement_rewrite_shape_compound_comma_and_whitespace() {
assert_eq!(
classify_requirement_rewrite_shape(">=1.2, <1.5"),
RequirementRewriteShape::Compound
);
assert_eq!(
classify_requirement_rewrite_shape(">=1.2.0 <2.0.0"),
RequirementRewriteShape::Compound
);
}
#[test]
fn test_classify_requirement_rewrite_shape_or_set_spaced_and_unspaced() {
assert_eq!(
classify_requirement_rewrite_shape("^1||^2"),
RequirementRewriteShape::Compound
);
assert_eq!(
classify_requirement_rewrite_shape("^1 || ^2"),
RequirementRewriteShape::Compound
);
}
#[test]
fn test_requirement_is_compound_ignores_operator_adjacent_whitespace() {
assert!(!requirement_is_compound("> 1.0"));
assert!(!requirement_is_compound("<= 1.5.0"));
assert!(requirement_is_compound(">=1.2.0 <2.0.0"));
assert!(requirement_is_compound(">=1.2, <1.5"));
}
#[test]
fn test_requirement_is_compound_ignores_spacing_after_non_angle_operators() {
assert!(!requirement_is_compound("= 1.6.13"));
assert!(!requirement_is_compound("~ 1.2.3"));
assert!(!requirement_is_compound("~> 1.2.3"));
assert!(!requirement_is_compound("^ 1.2.3"));
assert!(!requirement_is_compound("!= 1.0.0"));
}
#[test]
fn test_format_version_replacing_by_shape_preserves_exact_pin_and_tilde() {
let new_version = ConcreteVersion::new("2.0.0");
assert_eq!(
format_version_replacing_by_shape(&new_version, "=1.5.0", BareMeaning::Caret, || {
new_version.to_string()
}),
"=2.0.0"
);
assert_eq!(
format_version_replacing_by_shape(
&new_version,
"~1.5.0",
BareMeaning::ExactPin,
|| { new_version.to_string() }
),
"~2.0.0"
);
}
#[test]
fn test_format_version_replacing_by_shape_preserves_rubygems_tilde_spelling() {
let new_version = ConcreteVersion::new("2.0.0");
assert_eq!(
format_version_replacing_by_shape(
&new_version,
"~>1.5.0",
BareMeaning::ExactPin,
|| { new_version.to_string() }
),
"~>2.0.0"
);
}
#[test]
fn test_format_version_replacing_by_shape_caret_meaning_refuses_unsafe_shapes() {
let new_version = ConcreteVersion::new("2.0.0");
for requirement in ["1.2.*", "<1.5", ">=1.2, <1.5", ">=1.2.0 <2.0.0"] {
assert_eq!(
format_version_replacing_by_shape(
&new_version,
requirement,
BareMeaning::Caret,
|| new_version.to_string()
),
requirement,
"expected {requirement:?} to be echoed back unchanged under BareMeaning::Caret"
);
}
}
#[test]
fn test_format_version_replacing_by_shape_exact_pin_meaning_collapses_unsafe_shapes() {
let new_version = ConcreteVersion::new("2.0.0");
for requirement in ["1.2.*", "<1.5", ">=1.2, <1.5", ">=1.2.0 <2.0.0"] {
assert_eq!(
format_version_replacing_by_shape(
&new_version,
requirement,
BareMeaning::ExactPin,
|| new_version.to_string()
),
"2.0.0",
"expected {requirement:?} to collapse to bare under BareMeaning::ExactPin"
);
}
}
#[test]
fn test_format_version_replacing_by_shape_any_version_always_collapses() {
let new_version = ConcreteVersion::new("2.0.0");
for bare_meaning in [
BareMeaning::Caret,
BareMeaning::ExactPin,
BareMeaning::Floor,
] {
assert_eq!(
format_version_replacing_by_shape(&new_version, "*", bare_meaning, || {
new_version.to_string()
}),
"2.0.0"
);
}
}
#[test]
fn test_format_version_replacing_by_shape_bare_calls_bare_closure() {
let new_version = ConcreteVersion::new("2.0.0");
assert_eq!(
format_version_replacing_by_shape(&new_version, "1.5.0", BareMeaning::ExactPin, || {
new_version.as_str().to_string()
}),
"2.0.0"
);
}
#[test]
fn test_format_version_replacing_by_shape_explicit_caret_collapses_under_caret_meaning() {
let new_version = ConcreteVersion::new("2.0.0");
assert_eq!(
format_version_replacing_by_shape(&new_version, "^1.5.0", BareMeaning::Caret, || {
new_version.to_string()
}),
"2.0.0"
);
}
#[test]
fn test_format_version_replacing_by_shape_explicit_caret_preserved_under_exact_pin_meaning() {
let new_version = ConcreteVersion::new("2.0.0");
assert_eq!(
format_version_replacing_by_shape(
&new_version,
"^1.5.0",
BareMeaning::ExactPin,
|| { new_version.to_string() }
),
"^2.0.0"
);
}
#[test]
fn test_classify_requirement_rewrite_shape_bracket_exact_pin() {
assert_eq!(
classify_requirement_rewrite_shape("[1.0.0]"),
RequirementRewriteShape::BracketExactPin
);
}
#[test]
fn test_classify_requirement_rewrite_shape_bracket_range_is_compound_not_exact_pin() {
for requirement in ["[1.0,2.0)", "(1.0,2.0]", "[1.0,)", "(,1.0]"] {
assert_eq!(
classify_requirement_rewrite_shape(requirement),
RequirementRewriteShape::Compound,
"expected {requirement:?} to classify as Compound"
);
}
}
#[test]
fn test_classify_requirement_rewrite_shape_gradle_dynamic_plus_suffix() {
for requirement in ["1.0.+", "2.+", "1.2.3.+"] {
assert_eq!(
classify_requirement_rewrite_shape(requirement),
RequirementRewriteShape::PartialWildcard,
"expected {requirement:?} to classify as PartialWildcard"
);
}
}
#[test]
fn test_format_version_replacing_by_shape_floor_meaning_refuses_unsafe_shapes() {
let new_version = ConcreteVersion::new("13.0.4");
for requirement in ["[12.0.1,13.0.0)", "1.0.+", ">=1.0.0"] {
assert_eq!(
format_version_replacing_by_shape(
&new_version,
requirement,
BareMeaning::Floor,
|| new_version.to_string()
),
requirement,
"expected {requirement:?} to be echoed back unchanged under BareMeaning::Floor"
);
}
}
#[test]
fn test_format_version_replacing_by_shape_floor_meaning_preserves_bracket_exact_pin() {
let new_version = ConcreteVersion::new("13.0.4");
assert_eq!(
format_version_replacing_by_shape(&new_version, "[12.0.1]", BareMeaning::Floor, || {
new_version.to_string()
}),
"[13.0.4]"
);
}
#[test]
fn test_format_version_replacing_by_shape_floor_meaning_bare_still_collapses() {
let new_version = ConcreteVersion::new("13.0.4");
assert_eq!(
format_version_replacing_by_shape(&new_version, "12.0.1", BareMeaning::Floor, || {
new_version.to_string()
}),
"13.0.4"
);
}
#[test]
fn test_format_version_replacing_by_shape_floor_meaning_refuses_explicit_caret() {
let new_version = ConcreteVersion::new("13.0.4");
assert_eq!(
format_version_replacing_by_shape(&new_version, "^1.2.3", BareMeaning::Floor, || {
new_version.to_string()
}),
"^1.2.3"
);
}
#[test]
fn test_bare_meaning_covers_every_ecosystem() {
for &eco in EcosystemId::ALL {
let _ = bare_meaning(eco);
}
}
#[test]
fn test_caret_admits_up_to_date_rejects_compound_shapes() {
for requirement in ["^3 || ^4", "^4.5 <4.7", "^1 ^2", "^1, <2"] {
assert_eq!(
caret_admits_up_to_date("4.1.0", requirement),
None,
"{requirement}"
);
}
assert_eq!(caret_admits_up_to_date("4.1.0", "^4.0"), Some(true));
}
#[test]
fn test_up_to_date_via_compiled_matcher_falls_back_without_matcher() {
let latest = ConcreteVersion::new("1.2.3");
let requirement = VersionReq::new("1.2.3");
assert!(up_to_date_via_compiled_matcher(
&MOCK_FORMATTER,
BoundedVersionReq::new(&requirement).unwrap(),
&latest
));
}
struct MajorOneMatcher {
strict: bool,
}
impl RequirementMatcher for MajorOneMatcher {
fn matches(&self, version: &ConcreteVersion) -> Option<bool> {
Some(version.as_str().starts_with("1.") && !version.as_str().contains('-'))
}
fn strict_prerelease_exclusion(&self) -> bool {
self.strict
}
}
struct MajorOneFormatter {
strict: bool,
}
impl RequirementResolution for MajorOneFormatter {
fn compile_bounded_requirement(
&self,
_: BoundedVersionReq<'_>,
) -> Option<Box<dyn RequirementMatcher>> {
Some(Box::new(MajorOneMatcher {
strict: self.strict,
}))
}
}
#[test]
fn test_up_to_date_via_compiled_matcher_core_retry_needs_strict_matcher() {
let requirement = VersionReq::new(">=1 <2");
let bounded = BoundedVersionReq::new(&requirement).unwrap();
let latest = ConcreteVersion::new("1.5.0-beta.1");
assert!(up_to_date_via_compiled_matcher(
&MajorOneFormatter { strict: true },
bounded,
&latest
));
assert!(!up_to_date_via_compiled_matcher(
&MajorOneFormatter { strict: false },
bounded,
&latest
));
}
#[test]
fn test_requirement_needs_range_semantics() {
for requirement in [
"=1.2.3",
"<2",
">=1.2, <2",
" > 1",
"^1 || ^2",
"^1.5 <1.9",
">=1.*",
"=1.2.*",
] {
assert!(
requirement_needs_range_semantics(requirement),
"{requirement}"
);
}
for requirement in [
"1.0.228", "^1.5", "~1.2", "1.*", "1.2.*", "*", "1.0.+", "[1.0.0]",
] {
assert!(
!requirement_needs_range_semantics(requirement),
"{requirement}"
);
}
}
#[test]
fn test_up_to_date_via_compiled_matcher_relaxes_caret_floor_with_comma_grammar() {
struct CommaGrammarFormatter;
impl RequirementResolution for CommaGrammarFormatter {
fn compile_bounded_requirement(
&self,
requirement: BoundedVersionReq<'_>,
) -> Option<Box<dyn RequirementMatcher>> {
crate::lsp_helpers::compile_semver_requirement(requirement.get())
}
}
let up_to_date = |requirement: &str, latest: &str| {
let requirement = VersionReq::new(requirement);
up_to_date_via_compiled_matcher(
&CommaGrammarFormatter,
BoundedVersionReq::new(&requirement).unwrap(),
&ConcreteVersion::new(latest),
)
};
assert!(up_to_date("^1.5, <1.9", "1.4.9"));
assert!(!up_to_date("^1.5, <1.9", "1.9.5"));
}
#[test]
fn test_relax_caret_floors() {
assert_eq!(
relax_caret_floors("^1.5 <1.9", " ").as_deref(),
Some(">=1.0.0 <2.0.0 <1.9")
);
assert_eq!(
relax_caret_floors("^1.5,<1.9 || ^0.2", " ").as_deref(),
Some(">=1.0.0 <2.0.0,<1.9 || >=0.0.0 <0.3.0")
);
assert_eq!(
relax_caret_floors("^1.5, <1.9", ", ").as_deref(),
Some(">=1.0.0, <2.0.0, <1.9")
);
assert_eq!(relax_caret_floors("^x", " "), None);
assert_eq!(relax_caret_floors(">=1 <2", " "), None);
}
#[test]
fn test_default_osv_package_name_skips_empty_name() {
use crate::lsp_helpers::test_support::MockDep;
use crate::position::Range;
let dep = |name: &str| MockDep {
name: name.into(),
version_req: "1.0".into(),
version_range: Range::default(),
name_range: Range::default(),
};
assert_eq!(MOCK_FORMATTER.osv_package_name(&dep("")), None);
assert!(MOCK_FORMATTER.osv_package_name(&dep("serde")).is_some());
}
}