use std::collections::HashMap;
use crate::lsp_helpers::EcosystemFormatter;
use crate::{ConcreteVersion, Dependency, EcosystemId, PackageName};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BareRequirementPolicy {
AlwaysRange,
ConcreteIfFullVersion,
Concrete,
}
const fn bare_requirement_policy(ecosystem: EcosystemId) -> BareRequirementPolicy {
match ecosystem {
EcosystemId::Cargo | EcosystemId::Npm | EcosystemId::Composer | EcosystemId::Deno => {
BareRequirementPolicy::AlwaysRange
}
EcosystemId::GithubActions => BareRequirementPolicy::ConcreteIfFullVersion,
_ => BareRequirementPolicy::Concrete,
}
}
#[must_use]
pub fn is_full_semver_shape(s: &str) -> bool {
let s = s.strip_prefix(['v', 'V']).unwrap_or(s);
let core = match s.find(['-', '+']) {
Some(idx) => &s[..idx],
None => s,
};
let mut parts = core.split('.');
let (Some(major), Some(minor), Some(patch), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return false;
};
[major, minor, patch]
.iter()
.all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()))
}
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 => match bare_requirement_policy(ecosystem) {
BareRequirementPolicy::AlwaysRange => None,
BareRequirementPolicy::Concrete => {
looks_like_a_single_version(trimmed).then_some(trimmed)
}
BareRequirementPolicy::ConcreteIfFullVersion => {
is_full_semver_shape(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, ConcreteVersion>,
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()))
.map(ConcreteVersion::to_string)
.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);
}
#[test]
fn is_full_semver_shape_accepts_full_versions_with_and_without_v_prefix() {
assert!(is_full_semver_shape("4.2.0"));
assert!(is_full_semver_shape("v4.2.0"));
assert!(is_full_semver_shape("V4.2.0"));
}
#[test]
fn is_full_semver_shape_accepts_prerelease_and_build_suffixes() {
assert!(is_full_semver_shape("v4.2.0-beta.1"));
assert!(is_full_semver_shape("4.2.0+build.5"));
}
#[test]
fn is_full_semver_shape_rejects_partial_versions() {
assert!(!is_full_semver_shape("v4"));
assert!(!is_full_semver_shape("v4.2"));
}
#[test]
fn is_full_semver_shape_rejects_non_version_and_sha_shapes() {
assert!(!is_full_semver_shape("main"));
assert!(!is_full_semver_shape(
"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
));
assert!(!is_full_semver_shape(""));
assert!(!is_full_semver_shape("4.2.0.1"));
assert!(!is_full_semver_shape("4..0"));
}
#[test]
fn concrete_pin_version_github_actions_full_bare_tag_is_concrete() {
assert_eq!(
concrete_pin_version("v4.2.0", EcosystemId::GithubActions),
Some("v4.2.0")
);
}
#[test]
fn concrete_pin_version_github_actions_moving_major_tag_is_a_range() {
assert_eq!(concrete_pin_version("v4", EcosystemId::GithubActions), None);
assert_eq!(
concrete_pin_version("v4.2", EcosystemId::GithubActions),
None
);
}
#[test]
fn concrete_pin_version_github_actions_bare_sha_is_not_concrete() {
assert_eq!(
concrete_pin_version(
"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
EcosystemId::GithubActions
),
None
);
}
}