use std::collections::HashMap;
use crate::lsp_helpers::{BoundedVersionReq, EcosystemFormatter, ResolvedPin};
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 => BareRequirementPolicy::AlwaysRange,
EcosystemId::GithubActions
| EcosystemId::GitlabCi
| EcosystemId::Npm
| EcosystemId::Composer
| EcosystemId::Deno => BareRequirementPolicy::ConcreteIfFullVersion,
EcosystemId::Pypi
| EcosystemId::Go
| EcosystemId::Bundler
| EcosystemId::Dart
| EcosystemId::Maven
| EcosystemId::Gradle
| EcosystemId::Swift
| EcosystemId::NuGet => BareRequirementPolicy::Concrete,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PlusSuffixMeaning {
Dynamic,
PartOfVersion,
}
impl PlusSuffixMeaning {
fn accepts(self, s: &str) -> bool {
match self {
Self::Dynamic => !s.contains('+'),
Self::PartOfVersion => s.split_once('+').is_none_or(|(head, build)| {
!head.is_empty() && !build.is_empty() && build.bytes().all(|b| b.is_ascii_digit())
}),
}
}
}
const fn plus_suffix_meaning(ecosystem: EcosystemId) -> PlusSuffixMeaning {
match ecosystem {
EcosystemId::Dart => PlusSuffixMeaning::PartOfVersion,
EcosystemId::Cargo
| EcosystemId::GithubActions
| EcosystemId::GitlabCi
| EcosystemId::Npm
| EcosystemId::Composer
| EcosystemId::Deno
| EcosystemId::Pypi
| EcosystemId::Go
| EcosystemId::Bundler
| EcosystemId::Maven
| EcosystemId::Gradle
| EcosystemId::Swift
| EcosystemId::NuGet => PlusSuffixMeaning::Dynamic,
}
}
#[expect(
clippy::string_slice,
reason = "idx comes from str::find(['-', '+']), both ASCII bytes, so it is always a char \
boundary"
)]
#[must_use]
pub fn is_full_semver_shape(s: &str) -> bool {
let s = crate::github::normalize_tag(s);
let core = match s.find(['-', '+']) {
Some(idx) => &s[..idx],
None => s,
};
is_dotted_numeric::<3>(core)
}
fn is_dotted_numeric<const N: usize>(core: &str) -> bool {
let mut parts = core.split('.');
let components: [Option<&str>; N] = std::array::from_fn(|_| parts.next());
parts.next().is_none()
&& components
.iter()
.all(|p| p.is_some_and(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit())))
}
fn looks_like_a_single_version(s: &str, ecosystem: EcosystemId) -> bool {
if s.is_empty() {
return false;
}
if s.contains([
'^', '~', '*', '<', '>', ',', '|', '(', ')', '[', ']', ' ', '\t', ':',
]) {
return false;
}
if !plus_suffix_meaning(ecosystem).accepts(s) {
return false;
}
let core = crate::github::normalize_tag(s);
core.chars().next().is_some_and(|c| c.is_ascii_digit()) && !has_wildcard_component(core)
}
fn has_wildcard_component(version: &str) -> bool {
let (numeric, pre_release) = match version.split_once('-') {
Some((numeric, pre)) => (numeric, Some(pre)),
None => (version, None),
};
numeric.contains(['x', 'X'])
|| pre_release.is_some_and(|pre| pre.split('.').skip(1).any(|c| matches!(c, "x" | "X")))
}
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_circumfix('[', ']')
.filter(|inner| !inner.contains(','));
match pinned.or(bracket_pinned) {
Some(body) => looks_like_a_single_version(body, ecosystem).then_some(body),
None => match bare_requirement_policy(ecosystem) {
BareRequirementPolicy::AlwaysRange => None,
BareRequirementPolicy::Concrete => {
looks_like_a_single_version(trimmed, ecosystem).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()
}
fn version_matches_requirement(
formatter: &dyn EcosystemFormatter,
version: &ConcreteVersion,
requirement: BoundedVersionReq<'_>,
) -> bool {
if let Some(matcher) = formatter.compile_bounded_requirement(requirement) {
matcher.matches(version) == Some(true)
} else {
formatter.version_satisfies_bounded_requirement(version, requirement)
}
}
fn best_candidate_for_requirement<'a>(
candidates: &'a [ConcreteVersion],
requirement: &crate::VersionReq,
formatter: &dyn EcosystemFormatter,
) -> Option<&'a ConcreteVersion> {
let requirement = BoundedVersionReq::new(requirement)?;
candidates
.iter()
.filter(|v| version_matches_requirement(formatter, v, requirement))
.max_by(|a, b| crate::lockfile::compare_lockfile_versions(a.as_str(), b.as_str()))
}
pub(crate) fn resolve_occurrence_version<'a>(
dep: &dyn Dependency,
normalized_name: &str,
resolved_versions: &'a HashMap<PackageName, ConcreteVersion>,
resolved_version_candidates: Option<&'a HashMap<PackageName, Vec<ConcreteVersion>>>,
formatter: &dyn EcosystemFormatter,
) -> Option<&'a ConcreteVersion> {
let candidates = resolved_version_candidates.and_then(|candidates| {
candidates
.get(normalized_name)
.or_else(|| candidates.get(dep.name()))
});
match (candidates, dep.version_requirement()) {
(Some(candidates), Some(req)) if candidates.len() > 1 => {
best_candidate_for_requirement(candidates, req, formatter)
}
_ => resolved_versions
.get(normalized_name)
.or_else(|| resolved_versions.get(dep.name())),
}
}
pub fn resolve_in_use_version(
dep: &dyn Dependency,
normalized_name: &str,
resolved_versions: &HashMap<PackageName, ConcreteVersion>,
resolved_version_candidates: Option<&HashMap<PackageName, Vec<ConcreteVersion>>>,
formatter: &dyn EcosystemFormatter,
ecosystem: EcosystemId,
) -> Option<ConcreteVersion> {
resolve_in_use_versions(
dep,
normalized_name,
resolved_versions,
resolved_version_candidates,
formatter,
ecosystem,
)
.map(InUseVersions::into_primary)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InUseVersions {
primary: ConcreteVersion,
siblings: Vec<ConcreteVersion>,
}
impl InUseVersions {
fn without_siblings(primary: ConcreteVersion) -> Self {
Self {
primary,
siblings: Vec::new(),
}
}
#[cfg(any(test, feature = "test-util"))]
#[must_use]
pub const fn for_test(primary: ConcreteVersion, siblings: Vec<ConcreteVersion>) -> Self {
Self { primary, siblings }
}
#[must_use]
pub const fn primary(&self) -> &ConcreteVersion {
&self.primary
}
#[must_use]
pub fn siblings(&self) -> &[ConcreteVersion] {
&self.siblings
}
#[must_use]
pub fn into_primary(self) -> ConcreteVersion {
self.primary
}
}
pub fn resolve_in_use_versions(
dep: &dyn Dependency,
normalized_name: &str,
resolved_versions: &HashMap<PackageName, ConcreteVersion>,
resolved_version_candidates: Option<&HashMap<PackageName, Vec<ConcreteVersion>>>,
formatter: &dyn EcosystemFormatter,
ecosystem: EcosystemId,
) -> Option<InUseVersions> {
if formatter.manifest_requirement_is_resolved_version(dep) {
return dep
.version_requirement()
.and_then(|req| concrete_pin_version(req.as_str(), ecosystem))
.map(|v| InUseVersions::without_siblings(ConcreteVersion::from(v)));
}
if let Some(version) = resolve_occurrence_version(
dep,
normalized_name,
resolved_versions,
resolved_version_candidates,
formatter,
) {
return Some(InUseVersions::without_siblings(version.clone()));
}
if let Some(pin) = formatter.resolved_pin_version(dep)
&& let Some(primary) = queryable_pin_version(&pin, ecosystem)
{
let siblings = pin
.siblings()
.iter()
.filter_map(|tag| {
queryable_pin_version(&ResolvedPin::most_specific(tag.clone()), ecosystem)
})
.collect();
return Some(InUseVersions { primary, siblings });
}
dep.version_requirement()
.and_then(|req| concrete_pin_version(req.as_str(), ecosystem))
.map(|v| InUseVersions::without_siblings(ConcreteVersion::from(v)))
}
fn queryable_pin_version(pin: &ResolvedPin, ecosystem: EcosystemId) -> Option<ConcreteVersion> {
let version = pin.version();
match pin {
ResolvedPin::MostSpecific { .. } if is_major_minor_shape(version.as_str()) => {
Some(version.clone())
}
ResolvedPin::MostSpecific { .. } | ResolvedPin::Alias { .. } => {
concrete_pin_version(version.as_str(), ecosystem).map(ConcreteVersion::from)
}
}
}
fn is_major_minor_shape(s: &str) -> bool {
is_dotted_numeric::<2>(crate::github::normalize_tag(s))
}
#[must_use]
pub fn has_unqueryable_resolved_pin(
dep: &dyn Dependency,
formatter: &dyn EcosystemFormatter,
ecosystem: EcosystemId,
) -> bool {
formatter
.resolved_pin_version(dep)
.is_some_and(|pin| queryable_pin_version(&pin, ecosystem).is_none())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::position::Range;
struct CaretFormatter;
impl crate::lsp_helpers::PackageNaming for CaretFormatter {}
impl crate::lsp_helpers::PackageRendering for CaretFormatter {
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 crate::lsp_helpers::RequirementResolution for CaretFormatter {
fn compile_bounded_requirement(
&self,
requirement: BoundedVersionReq<'_>,
) -> Option<Box<dyn crate::lsp_helpers::RequirementMatcher>> {
let requirement = requirement.get();
crate::lsp_helpers::compile_semver_requirement(requirement)
}
}
impl crate::lsp_helpers::DiagnosticMessages for CaretFormatter {}
impl crate::lsp_helpers::DiagnosticPolicy for CaretFormatter {}
impl crate::lsp_helpers::SourcePolicy for CaretFormatter {}
impl crate::lsp_helpers::OsvNaming for CaretFormatter {}
struct FixedResolvedPinFormatter(ResolvedPin);
impl FixedResolvedPinFormatter {
fn most_specific(tag: &str) -> Self {
Self(ResolvedPin::most_specific(ConcreteVersion::new(tag)))
}
fn alias(tag: &str) -> Self {
Self(ResolvedPin::alias(ConcreteVersion::new(tag)))
}
fn from_index(tags: &[&str]) -> Self {
let sha = crate::lsp_helpers::CommitSha::parse(&"a".repeat(40)).unwrap();
let index = crate::lsp_helpers::TagIndex::from_tags(tags.iter().map(|t| (*t, &sha)));
Self(index.resolved_pin(sha.as_str()).unwrap())
}
}
impl crate::lsp_helpers::PackageNaming for FixedResolvedPinFormatter {}
impl crate::lsp_helpers::PackageRendering for FixedResolvedPinFormatter {
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 crate::lsp_helpers::RequirementResolution for FixedResolvedPinFormatter {
fn resolved_pin_version(&self, _dep: &dyn Dependency) -> Option<ResolvedPin> {
Some(self.0.clone())
}
}
impl crate::lsp_helpers::DiagnosticMessages for FixedResolvedPinFormatter {}
impl crate::lsp_helpers::DiagnosticPolicy for FixedResolvedPinFormatter {}
impl crate::lsp_helpers::SourcePolicy for FixedResolvedPinFormatter {}
impl crate::lsp_helpers::OsvNaming for FixedResolvedPinFormatter {}
#[test]
fn resolve_in_use_version_rejects_hook_output_that_is_not_full_semver_shape() {
use crate::VersionReq;
use crate::lsp_helpers::test_support::MockDep;
let dep = MockDep {
name: PackageName::new("actions/checkout"),
version_req: VersionReq::new("deadbeef"),
version_range: Range::default(),
name_range: Range::default(),
};
let unqueryable = [
FixedResolvedPinFormatter::most_specific("v1"),
FixedResolvedPinFormatter::most_specific("cargo-deny"),
FixedResolvedPinFormatter::alias("v1"),
FixedResolvedPinFormatter::alias("2.9"),
];
for formatter in &unqueryable {
let result = resolve_in_use_version(
&dep,
"actions/checkout",
&HashMap::new(),
None,
formatter,
EcosystemId::GithubActions,
);
assert_eq!(
result, None,
"{:?} must not resolve as concrete",
formatter.0
);
assert!(has_unqueryable_resolved_pin(
&dep,
formatter,
EcosystemId::GithubActions
));
}
}
#[test]
fn resolve_in_use_version_accepts_most_specific_two_component_tag() {
use crate::VersionReq;
use crate::lsp_helpers::test_support::MockDep;
let dep = MockDep {
name: PackageName::new("obi1kenobi/cargo-semver-checks-action"),
version_req: VersionReq::new("deadbeef"),
version_range: Range::default(),
name_range: Range::default(),
};
for (formatter, expected) in [
(
FixedResolvedPinFormatter::most_specific("v2.9"),
Some(ConcreteVersion::from("v2.9")),
),
(FixedResolvedPinFormatter::alias("v2.9"), None),
] {
let result = resolve_in_use_version(
&dep,
"obi1kenobi/cargo-semver-checks-action",
&HashMap::new(),
None,
&formatter,
EcosystemId::GithubActions,
);
assert_eq!(result, expected, "{:?}", formatter.0);
}
}
#[test]
fn resolve_in_use_version_lockfile_wins_over_resolved_pin_version_hook() {
use crate::VersionReq;
use crate::lsp_helpers::test_support::MockDep;
let dep = MockDep {
name: PackageName::new("actions/checkout"),
version_req: VersionReq::new("deadbeef"),
version_range: Range::default(),
name_range: Range::default(),
};
let mut resolved_versions = HashMap::new();
resolved_versions.insert(
PackageName::new("actions/checkout"),
ConcreteVersion::from("v9.9.9"),
);
let result = resolve_in_use_version(
&dep,
"actions/checkout",
&resolved_versions,
None,
&FixedResolvedPinFormatter::most_specific("v1.3.0"),
EcosystemId::GithubActions,
);
assert_eq!(
result,
Some(ConcreteVersion::from("v9.9.9")),
"the lock-file-resolved version must win over resolved_pin_version's hook output"
);
}
#[test]
fn resolve_in_use_version_accepts_hook_output_that_is_full_semver_shape() {
use crate::VersionReq;
use crate::lsp_helpers::test_support::MockDep;
let dep = MockDep {
name: PackageName::new("actions/checkout"),
version_req: VersionReq::new("deadbeef"),
version_range: Range::default(),
name_range: Range::default(),
};
let result = resolve_in_use_version(
&dep,
"actions/checkout",
&HashMap::new(),
None,
&FixedResolvedPinFormatter::most_specific("v1.3.0"),
EcosystemId::GithubActions,
);
assert_eq!(result, Some(ConcreteVersion::from("v1.3.0")));
}
#[test]
fn is_major_minor_shape_accepts_only_bare_two_component_versions() {
for accepted in ["v2.9", "V2.9", "2.9", "v10.0"] {
assert!(is_major_minor_shape(accepted), "{accepted:?}");
}
for rejected in ["v2.9-rc1", "v2.", "v.9", "v2.9.0", "v2", "", "v2.x", "main"] {
assert!(!is_major_minor_shape(rejected), "{rejected:?}");
}
}
#[test]
fn has_unqueryable_resolved_pin_distinguishes_partial_tag_from_no_resolution() {
use crate::VersionReq;
use crate::lsp_helpers::test_support::MockDep;
let dep = MockDep {
name: PackageName::new("actions/checkout"),
version_req: VersionReq::new("deadbeef"),
version_range: Range::default(),
name_range: Range::default(),
};
for (formatter, expected) in [
(FixedResolvedPinFormatter::most_specific("v2.9-rc1"), true),
(FixedResolvedPinFormatter::most_specific("v2"), true),
(FixedResolvedPinFormatter::alias("v2.9"), true),
(FixedResolvedPinFormatter::most_specific("v2.9"), false),
(FixedResolvedPinFormatter::most_specific("v1.3.0"), false),
] {
assert_eq!(
has_unqueryable_resolved_pin(&dep, &formatter, EcosystemId::GithubActions),
expected,
"{:?}",
formatter.0
);
}
assert!(!has_unqueryable_resolved_pin(
&dep,
&CaretFormatter,
EcosystemId::GithubActions
));
}
#[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_dart_plus_build_number_is_a_pin() {
assert_eq!(
concrete_pin_version("0.8.13+1", EcosystemId::Dart),
Some("0.8.13+1")
);
assert_eq!(
concrete_pin_version("1.0.0-beta.2+3", EcosystemId::Dart),
Some("1.0.0-beta.2+3")
);
}
#[test]
fn is_concrete_version_dart_malformed_plus_is_rejected() {
for req in [
"1.+",
"4.+",
"1+",
"1.0.0+",
"1.0.0+1+2",
"1.0+local",
"1.2.3+build",
] {
assert!(!is_concrete_version(req, EcosystemId::Dart), "{req}");
}
}
#[test]
fn is_concrete_version_dart_plus_ranges_stay_non_exact() {
for req in ["^0.8.13+1", ">=1.0.0+2", ">=1.0.0+2 <2.0.0", "any"] {
assert!(!is_concrete_version(req, EcosystemId::Dart), "{req}");
}
}
#[test]
fn is_concrete_version_plus_still_dynamic_for_gradle_and_others() {
for req in ["1.+", "4.+", "1.2.3+1"] {
assert!(!is_concrete_version(req, EcosystemId::Gradle), "{req}");
}
for &eco in EcosystemId::ALL.iter().filter(|&&e| e != EcosystemId::Dart) {
assert!(!is_concrete_version("==0.8.13+1", eco), "{eco:?}");
}
}
#[test]
fn is_concrete_version_nuget_bare_version_is_a_deliberate_pin_approximation() {
assert!(is_concrete_version("1.0.0", EcosystemId::NuGet));
assert!(is_concrete_version("[1.0.0]", EcosystemId::NuGet));
}
#[test]
fn is_concrete_version_bare_digit_rejected_for_range_default_ecosystems() {
assert!(!is_concrete_version("1.2.3", EcosystemId::Cargo));
assert!(is_concrete_version("=1.2.3", EcosystemId::Cargo));
}
#[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_accepts_pins_containing_the_letter_x() {
for eco in [EcosystemId::Go, EcosystemId::Dart, EcosystemId::Npm] {
for pin in [
"0.1.0-experimental",
"2.0.0-dev.x1",
"1.0.0-next",
"1.2.3-x",
"1.2.3-x.1",
"1.0.0-max",
"1.0.0-xx",
] {
assert!(is_concrete_version(pin, eco), "{pin} {eco:?}");
}
}
}
#[test]
fn is_concrete_version_rejects_wildcard_components() {
for eco in [EcosystemId::Go, EcosystemId::Dart] {
for wildcard in [
"1.x",
"1.2.X",
"1.*",
"x",
"X",
"x.1",
"v1.x",
"1..x",
"1.x.0",
"1.2.xx",
"1.x0",
"1.2.x-SNAPSHOT",
"1.2.x-beta",
"1.x-beta",
"2.x-dev",
"1.2.3-alpha.x",
"1.0.0-rc.x",
] {
assert!(!is_concrete_version(wildcard, eco), "{wildcard} {eco:?}");
}
}
}
#[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
);
}
#[test]
fn concrete_pin_version_gitlab_ci_full_tag_is_concrete() {
assert_eq!(
concrete_pin_version("1.2.3", EcosystemId::GitlabCi),
Some("1.2.3")
);
assert_eq!(
concrete_pin_version("v1.2.3", EcosystemId::GitlabCi),
Some("v1.2.3")
);
}
#[test]
fn concrete_pin_version_gitlab_ci_partial_pin_is_a_range() {
assert_eq!(concrete_pin_version("1.2", EcosystemId::GitlabCi), None);
assert_eq!(concrete_pin_version("1", EcosystemId::GitlabCi), None);
}
#[test]
fn concrete_pin_version_gitlab_ci_digit_leading_sha_is_not_concrete() {
assert_eq!(
concrete_pin_version(
"1234567890abcdef1234567890abcdef12345678",
EcosystemId::GitlabCi
),
None
);
}
#[test]
fn concrete_pin_version_npm_full_bare_version_is_concrete() {
assert_eq!(
concrete_pin_version("4.17.0", EcosystemId::Npm),
Some("4.17.0")
);
}
#[test]
fn concrete_pin_version_npm_partial_bare_version_is_a_range() {
assert_eq!(concrete_pin_version("4.17", EcosystemId::Npm), None);
assert_eq!(concrete_pin_version("4", EcosystemId::Npm), None);
}
#[test]
fn concrete_pin_version_npm_explicit_pin_still_concrete() {
assert_eq!(
concrete_pin_version("=4.17.0", EcosystemId::Npm),
Some("4.17.0")
);
}
#[test]
fn concrete_pin_version_composer_full_bare_version_is_concrete() {
assert_eq!(
concrete_pin_version("2.0.0", EcosystemId::Composer),
Some("2.0.0")
);
}
#[test]
fn concrete_pin_version_composer_partial_bare_version_is_a_range() {
assert_eq!(concrete_pin_version("2.0", EcosystemId::Composer), None);
assert_eq!(concrete_pin_version("2", EcosystemId::Composer), None);
}
#[test]
fn concrete_pin_version_composer_explicit_pin_still_concrete() {
assert_eq!(
concrete_pin_version("=2.0.0", EcosystemId::Composer),
Some("2.0.0")
);
}
#[test]
fn concrete_pin_version_deno_full_bare_version_is_concrete() {
assert_eq!(
concrete_pin_version("4.17.0", EcosystemId::Deno),
Some("4.17.0")
);
}
#[test]
fn concrete_pin_version_deno_partial_bare_version_is_a_range() {
assert_eq!(concrete_pin_version("4.17", EcosystemId::Deno), None);
assert_eq!(concrete_pin_version("4", EcosystemId::Deno), None);
}
#[test]
fn concrete_pin_version_deno_explicit_pin_still_concrete() {
assert_eq!(
concrete_pin_version("=1.0.0", EcosystemId::Deno),
Some("1.0.0")
);
}
#[test]
fn in_use_version_renamed_occurrence_resolves_its_own_major() {
use crate::VersionReq;
use crate::lsp_helpers::test_support::MockDep;
let renamed_old_major = MockDep {
name: PackageName::new("serde"),
version_req: VersionReq::new("0.9"),
version_range: Range::default(),
name_range: Range::default(),
};
let plain_current_major = MockDep {
name: PackageName::new("serde"),
version_req: VersionReq::new("1.0"),
version_range: Range::default(),
name_range: Range::default(),
};
let mut resolved_versions = HashMap::new();
resolved_versions.insert(PackageName::new("serde"), ConcreteVersion::from("1.0.219"));
let mut candidates = HashMap::new();
candidates.insert(
PackageName::new("serde"),
vec![
ConcreteVersion::from("0.9.15"),
ConcreteVersion::from("1.0.219"),
],
);
let renamed_result = resolve_in_use_version(
&renamed_old_major,
"serde",
&resolved_versions,
Some(&candidates),
&crate::lsp_helpers::test_support::MOCK_FORMATTER,
EcosystemId::Cargo,
);
let plain_result = resolve_in_use_version(
&plain_current_major,
"serde",
&resolved_versions,
Some(&candidates),
&crate::lsp_helpers::test_support::MOCK_FORMATTER,
EcosystemId::Cargo,
);
assert_eq!(renamed_result, Some(ConcreteVersion::from("0.9.15")));
assert_eq!(plain_result, Some(ConcreteVersion::from("1.0.219")));
}
#[test]
fn in_use_version_no_candidate_satisfies_requirement_returns_none() {
use crate::VersionReq;
use crate::lsp_helpers::test_support::MockDep;
let dep = MockDep {
name: PackageName::new("serde"),
version_req: VersionReq::new("0.9"),
version_range: Range::default(),
name_range: Range::default(),
};
let resolved_versions = HashMap::new();
let mut candidates = HashMap::new();
candidates.insert(
PackageName::new("serde"),
vec![
ConcreteVersion::from("1.0.219"),
ConcreteVersion::from("1.1.0"),
],
);
let result = resolve_in_use_version(
&dep,
"serde",
&resolved_versions,
Some(&candidates),
&crate::lsp_helpers::test_support::MOCK_FORMATTER,
EcosystemId::Cargo,
);
assert_eq!(result, None);
}
#[test]
fn in_use_version_no_requirement_falls_back_to_collapsed_value() {
use crate::lsp_helpers::test_support::MockMarkedDep;
let dep = MockMarkedDep {
name: PackageName::new("serde"),
name_range: Range::default(),
markers: None,
};
let mut resolved_versions = HashMap::new();
resolved_versions.insert(PackageName::new("serde"), ConcreteVersion::from("1.0.219"));
let mut candidates = HashMap::new();
candidates.insert(
PackageName::new("serde"),
vec![
ConcreteVersion::from("0.9.15"),
ConcreteVersion::from("1.0.219"),
],
);
let result = resolve_in_use_version(
&dep,
"serde",
&resolved_versions,
Some(&candidates),
&crate::lsp_helpers::test_support::MOCK_FORMATTER,
EcosystemId::Cargo,
);
assert_eq!(result, Some(ConcreteVersion::from("1.0.219")));
}
#[test]
fn in_use_version_single_candidate_matches_collapsed_fast_path() {
use crate::VersionReq;
use crate::lsp_helpers::test_support::MockDep;
let dep = MockDep {
name: PackageName::new("serde"),
version_req: VersionReq::new("1.0"),
version_range: Range::default(),
name_range: Range::default(),
};
let mut resolved_versions = HashMap::new();
resolved_versions.insert(PackageName::new("serde"), ConcreteVersion::from("1.0.219"));
let mut candidates = HashMap::new();
candidates.insert(
PackageName::new("serde"),
vec![ConcreteVersion::from("1.0.219")],
);
let with_candidates = resolve_in_use_version(
&dep,
"serde",
&resolved_versions,
Some(&candidates),
&crate::lsp_helpers::test_support::MOCK_FORMATTER,
EcosystemId::Cargo,
);
let without_candidates = resolve_in_use_version(
&dep,
"serde",
&resolved_versions,
None,
&crate::lsp_helpers::test_support::MOCK_FORMATTER,
EcosystemId::Cargo,
);
assert_eq!(with_candidates, Some(ConcreteVersion::from("1.0.219")));
assert_eq!(with_candidates, without_candidates);
}
#[test]
fn best_candidate_for_requirement_non_semver_uses_lexicographic_tiebreak() {
let candidates = vec![
ConcreteVersion::from("1-rc1"),
ConcreteVersion::from("1-rc2"),
];
let result = best_candidate_for_requirement(
&candidates,
&crate::VersionReq::new("1"),
&crate::lsp_helpers::test_support::MOCK_FORMATTER,
);
assert_eq!(result, Some(&ConcreteVersion::from("1-rc2")));
}
#[test]
fn best_candidate_for_requirement_uses_compile_requirement_when_available() {
use crate::VersionReq;
let candidates = vec![
ConcreteVersion::from("1.3.2"),
ConcreteVersion::from("2.9.4"),
];
let result =
best_candidate_for_requirement(&candidates, &VersionReq::new("2.4"), &CaretFormatter);
assert_eq!(result, Some(&ConcreteVersion::from("2.9.4")));
}
#[test]
fn best_candidate_for_requirement_picks_highest_of_satisfying_subset() {
use crate::VersionReq;
let candidates = vec![
ConcreteVersion::from("1.0.1"),
ConcreteVersion::from("1.0.5"),
ConcreteVersion::from("2.0.0"),
];
let result =
best_candidate_for_requirement(&candidates, &VersionReq::new("^1.0"), &CaretFormatter);
assert_eq!(result, Some(&ConcreteVersion::from("1.0.5")));
}
#[test]
fn best_candidate_for_requirement_oversized_requirement_returns_none() {
use crate::VersionReq;
let oversized = "1".repeat(300);
let candidates = vec![ConcreteVersion::from(oversized.as_str())];
let result = best_candidate_for_requirement(
&candidates,
&VersionReq::new(oversized),
&crate::lsp_helpers::test_support::MOCK_FORMATTER,
);
assert_eq!(result, None);
}
#[test]
fn in_use_version_tiebreaks_among_multiple_satisfying_candidates() {
use crate::VersionReq;
use crate::lsp_helpers::test_support::MockDep;
let dep = MockDep {
name: PackageName::new("pkg"),
version_req: VersionReq::new("^1.0"),
version_range: Range::default(),
name_range: Range::default(),
};
let resolved_versions = HashMap::new();
let mut candidates = HashMap::new();
candidates.insert(
PackageName::new("pkg"),
vec![
ConcreteVersion::from("1.0.1"),
ConcreteVersion::from("1.0.5"),
ConcreteVersion::from("2.0.0"),
],
);
let result = resolve_in_use_version(
&dep,
"pkg",
&resolved_versions,
Some(&candidates),
&CaretFormatter,
EcosystemId::Cargo,
);
assert_eq!(result, Some(ConcreteVersion::from("1.0.5")));
}
#[test]
fn in_use_version_npm_bare_full_version_no_lockfile_resolves_664_repro() {
use crate::VersionReq;
use crate::lsp_helpers::test_support::MockDep;
let dep = MockDep {
name: PackageName::new("express"),
version_req: VersionReq::new("4.17.0"),
version_range: Range::default(),
name_range: Range::default(),
};
let result = resolve_in_use_version(
&dep,
"express",
&HashMap::new(),
None,
&crate::lsp_helpers::test_support::MOCK_FORMATTER,
EcosystemId::Npm,
);
assert_eq!(result, Some(ConcreteVersion::from("4.17.0")));
}
#[test]
fn in_use_version_composer_bare_full_version_no_lockfile_resolves_664_repro() {
use crate::VersionReq;
use crate::lsp_helpers::test_support::MockDep;
let dep = MockDep {
name: PackageName::new("monolog/monolog"),
version_req: VersionReq::new("2.0.0"),
version_range: Range::default(),
name_range: Range::default(),
};
let result = resolve_in_use_version(
&dep,
"monolog/monolog",
&HashMap::new(),
None,
&crate::lsp_helpers::test_support::MOCK_FORMATTER,
EcosystemId::Composer,
);
assert_eq!(result, Some(ConcreteVersion::from("2.0.0")));
}
#[test]
fn in_use_version_cargo_bare_full_version_no_lockfile_still_none() {
use crate::VersionReq;
use crate::lsp_helpers::test_support::MockDep;
let dep = MockDep {
name: PackageName::new("serde"),
version_req: VersionReq::new("1.0.219"),
version_range: Range::default(),
name_range: Range::default(),
};
let result = resolve_in_use_version(
&dep,
"serde",
&HashMap::new(),
None,
&crate::lsp_helpers::test_support::MOCK_FORMATTER,
EcosystemId::Cargo,
);
assert_eq!(result, None);
}
#[test]
fn in_use_version_deno_bare_full_version_no_lockfile_resolves_667_repro() {
use crate::VersionReq;
use crate::lsp_helpers::test_support::MockDep;
let dep = MockDep {
name: PackageName::new("express"),
version_req: VersionReq::new("4.17.0"),
version_range: Range::default(),
name_range: Range::default(),
};
let result = resolve_in_use_version(
&dep,
"express",
&HashMap::new(),
None,
&crate::lsp_helpers::test_support::MOCK_FORMATTER,
EcosystemId::Deno,
);
assert_eq!(result, Some(ConcreteVersion::from("4.17.0")));
}
#[test]
fn in_use_version_nuget_bare_floor_resolves_as_pin_approximation() {
use crate::VersionReq;
use crate::lsp_helpers::test_support::MockDep;
let dep = MockDep {
name: PackageName::new("Newtonsoft.Json"),
version_req: VersionReq::new("1.0.0"),
version_range: Range::default(),
name_range: Range::default(),
};
let result = resolve_in_use_version(
&dep,
"newtonsoft.json",
&HashMap::new(),
None,
&crate::lsp_helpers::test_support::MOCK_FORMATTER,
EcosystemId::NuGet,
);
assert_eq!(result, Some(ConcreteVersion::from("1.0.0")));
}
fn sha_pinned_dep() -> crate::lsp_helpers::test_support::MockDep {
crate::lsp_helpers::test_support::MockDep {
name: PackageName::new("actions/checkout"),
version_req: crate::VersionReq::new("deadbeef"),
version_range: Range::default(),
name_range: Range::default(),
}
}
#[test]
fn resolve_in_use_versions_reports_pin_siblings_through_the_queryable_gate() {
let formatter = FixedResolvedPinFormatter::from_index(&[
"v4.9.0",
"v4.8.0",
"v4.10.0",
"v4.11.0-rc1",
"v4",
]);
let versions = resolve_in_use_versions(
&sha_pinned_dep(),
"actions/checkout",
&HashMap::new(),
None,
&formatter,
EcosystemId::GithubActions,
)
.unwrap();
assert_eq!(versions.primary().as_str(), "v4.8.0");
let siblings: Vec<&str> = versions.siblings().iter().map(|s| s.as_str()).collect();
assert_eq!(siblings, ["v4.9.0", "v4.10.0"]);
}
#[test]
fn resolve_in_use_versions_two_component_primary_keeps_two_component_siblings() {
let formatter = FixedResolvedPinFormatter::from_index(&["v2.9", "v2.10"]);
let versions = resolve_in_use_versions(
&sha_pinned_dep(),
"actions/checkout",
&HashMap::new(),
None,
&formatter,
EcosystemId::GithubActions,
)
.unwrap();
assert_eq!(versions.primary().as_str(), "v2.9");
assert_eq!(versions.siblings().len(), 1);
}
#[test]
fn resolve_in_use_versions_lockfile_version_has_no_siblings() {
let formatter = FixedResolvedPinFormatter::from_index(&["v4.8.0", "v4.9.0"]);
let resolved = HashMap::from([(
PackageName::new("actions/checkout"),
ConcreteVersion::new("4.7.0"),
)]);
let versions = resolve_in_use_versions(
&sha_pinned_dep(),
"actions/checkout",
&resolved,
None,
&formatter,
EcosystemId::GithubActions,
)
.unwrap();
assert_eq!(versions.primary().as_str(), "4.7.0");
assert!(versions.siblings().is_empty());
}
#[test]
fn resolve_in_use_version_returns_the_primary_of_a_sibling_pin() {
let formatter = FixedResolvedPinFormatter::from_index(&["v4.8.0", "v4.9.0"]);
assert_eq!(
resolve_in_use_version(
&sha_pinned_dep(),
"actions/checkout",
&HashMap::new(),
None,
&formatter,
EcosystemId::GithubActions,
),
Some(ConcreteVersion::new("v4.8.0"))
);
}
#[test]
fn bare_meaning_agrees_with_bare_requirement_policy_on_always_range() {
use crate::lsp_helpers::{BareMeaning, bare_meaning};
for &eco in EcosystemId::ALL {
let policy = bare_requirement_policy(eco);
let meaning = bare_meaning(eco);
match policy {
BareRequirementPolicy::AlwaysRange => assert_eq!(
meaning,
BareMeaning::Caret,
"{eco:?}: AlwaysRange (bare is an auto-following range for in-use-version \
resolution) must also be BareMeaning::Caret for rewrite-safety purposes"
),
BareRequirementPolicy::ConcreteIfFullVersion | BareRequirementPolicy::Concrete => {
assert_ne!(
meaning,
BareMeaning::Caret,
"{eco:?}: a bare version usable as an in-use-version pin ({policy:?}) \
cannot also carry BareMeaning::Caret rewrite semantics — an ecosystem \
whose bare form doesn't auto-follow future releases cannot widen when a \
bounded range collapses to bare either"
);
}
}
}
}
}