use std::collections::HashMap;
use crate::lsp_helpers::EcosystemFormatter;
use crate::{Dependency, EcosystemId, PackageName};
const fn bare_version_is_a_range(ecosystem: EcosystemId) -> bool {
matches!(
ecosystem,
EcosystemId::Cargo | EcosystemId::Npm | EcosystemId::Composer | EcosystemId::Deno
)
}
fn looks_like_a_single_version(s: &str) -> bool {
if s.is_empty() {
return false;
}
if s.contains([
'^', '~', '*', '<', '>', ',', '|', '(', ')', '[', ']', ' ', '\t', ':', '+', 'x', 'X',
]) {
return false;
}
let core = s.strip_prefix(['v', 'V']).unwrap_or(s);
core.chars().next().is_some_and(|c| c.is_ascii_digit())
}
pub fn concrete_pin_version(requirement: &str, ecosystem: EcosystemId) -> Option<&str> {
let trimmed = requirement.trim();
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("latest") {
return None;
}
let pinned = trimmed
.strip_prefix("==")
.or_else(|| trimmed.strip_prefix('='));
let bracket_pinned = trimmed
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.filter(|inner| !inner.contains(','));
match pinned.or(bracket_pinned) {
Some(body) => looks_like_a_single_version(body).then_some(body),
None if bare_version_is_a_range(ecosystem) => None,
None => looks_like_a_single_version(trimmed).then_some(trimmed),
}
}
#[cfg(test)]
fn is_concrete_version(requirement: &str, ecosystem: EcosystemId) -> bool {
concrete_pin_version(requirement, ecosystem).is_some()
}
pub fn in_use_version(
dep: &dyn Dependency,
normalized_name: &str,
resolved_versions: &HashMap<PackageName, String>,
formatter: &dyn EcosystemFormatter,
ecosystem: EcosystemId,
) -> Option<String> {
if formatter.manifest_requirement_is_resolved_version(dep) {
dep.version_requirement()
.and_then(|req| concrete_pin_version(req.as_str(), ecosystem))
.map(str::to_string)
} else {
resolved_versions
.get(normalized_name)
.or_else(|| resolved_versions.get(dep.name()))
.cloned()
.or_else(|| {
dep.version_requirement()
.and_then(|req| concrete_pin_version(req.as_str(), ecosystem))
.map(str::to_string)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_concrete_version_accepts_explicit_pins_in_any_ecosystem() {
for eco in [EcosystemId::Cargo, EcosystemId::Npm, EcosystemId::Go] {
assert!(is_concrete_version("=1.2.3", eco), "{eco:?}");
}
assert!(is_concrete_version("v1.9.1", EcosystemId::Go));
}
#[test]
fn is_concrete_version_pep440_double_equals_is_a_pin() {
assert!(is_concrete_version("==2.28.0", EcosystemId::Pypi));
}
#[test]
fn is_concrete_version_bare_digit_accepted_for_non_range_default_ecosystems() {
for eco in [
EcosystemId::Maven,
EcosystemId::Go,
EcosystemId::Bundler,
EcosystemId::Dart,
EcosystemId::Gradle,
EcosystemId::NuGet,
] {
assert!(is_concrete_version("2.14.1", eco), "{eco:?}");
}
}
#[test]
fn is_concrete_version_bare_digit_rejected_for_range_default_ecosystems() {
for eco in [
EcosystemId::Cargo,
EcosystemId::Npm,
EcosystemId::Composer,
EcosystemId::Deno,
] {
assert!(!is_concrete_version("1.2.3", eco), "{eco:?}");
assert!(is_concrete_version("=1.2.3", eco), "{eco:?}");
}
}
#[test]
fn is_concrete_version_rejects_partials_and_wildcards() {
for eco in [EcosystemId::Npm, EcosystemId::Composer] {
assert!(!is_concrete_version("1.x", eco), "{eco:?}");
assert!(!is_concrete_version("1.2.x", eco), "{eco:?}");
assert!(!is_concrete_version("1.2", eco), "{eco:?}");
}
assert!(!is_concrete_version("1.+", EcosystemId::Gradle));
}
#[test]
fn is_concrete_version_rejects_ranges_and_wildcards() {
for eco in [EcosystemId::Maven, EcosystemId::Go] {
assert!(!is_concrete_version("^1.0", eco));
assert!(!is_concrete_version("~1.2", eco));
assert!(!is_concrete_version("*", eco));
assert!(!is_concrete_version(">=1.0", eco));
assert!(!is_concrete_version(">=1.0 <2.0", eco));
assert!(!is_concrete_version("1.0.*", eco));
assert!(!is_concrete_version("", eco));
}
}
#[test]
fn is_concrete_version_rejects_non_version_schemes() {
let eco = EcosystemId::Go;
assert!(!is_concrete_version("latest", eco));
assert!(!is_concrete_version("github:user/repo", eco));
assert!(!is_concrete_version("file:../x", eco));
assert!(!is_concrete_version("main", eco));
}
#[test]
fn concrete_pin_version_strips_pep440_double_equals_comparator() {
assert_eq!(
concrete_pin_version("==4.9.0", EcosystemId::Pypi),
Some("4.9.0")
);
}
#[test]
fn concrete_pin_version_strips_single_equals_and_bracket_pins() {
assert_eq!(
concrete_pin_version("=1.2.3", EcosystemId::Cargo),
Some("1.2.3")
);
assert_eq!(
concrete_pin_version("[1.0.0]", EcosystemId::NuGet),
Some("1.0.0")
);
}
#[test]
fn concrete_pin_version_bare_version_returned_verbatim() {
assert_eq!(
concrete_pin_version("2.14.1", EcosystemId::Maven),
Some("2.14.1")
);
}
#[test]
fn concrete_pin_version_rejects_ranges_and_partials() {
assert_eq!(concrete_pin_version("^1.0", EcosystemId::Cargo), None);
assert_eq!(concrete_pin_version("1.2.3", EcosystemId::Cargo), None);
assert_eq!(concrete_pin_version(">=1.0,<2.0", EcosystemId::Pypi), None);
}
}