Skip to content

Commit 4a06c2c

Browse files
committed
fix(core): handle specifiers like =9.0.0
Closes #239
1 parent 0fd7c9d commit 4a06c2c

6 files changed

Lines changed: 127 additions & 5 deletions

File tree

src/specifier.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,8 @@ impl Specifier {
484484

485485
match self {
486486
Self::Exact(s) => {
487-
let new_specifier = format!("{}{}", range_str, s.raw);
487+
// Use node_version to get clean version without = prefix
488+
let new_specifier = format!("{}{}", range_str, s.node_version);
488489
Some(Self::new(&new_specifier))
489490
}
490491
Self::Major(s) => {

src/specifier/exact.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ pub struct Exact {
1616

1717
impl Exact {
1818
pub fn create(raw: &str) -> Specifier {
19-
match Specifier::new_node_version(raw) {
19+
// Strip leading = if present (npm allows =1.2.3 as equivalent to 1.2.3)
20+
let version_without_equals = raw.strip_prefix('=').unwrap_or(raw);
21+
22+
match Specifier::new_node_version(version_without_equals) {
2023
Some(node_version) => {
21-
let node_range = Specifier::new_node_range(raw).unwrap_or_else(|| {
24+
let node_range = Specifier::new_node_range(version_without_equals).unwrap_or_else(|| {
2225
// Fallback: should never happen if node_version parses
23-
Rc::new(node_semver::Range::parse(raw).unwrap())
26+
Rc::new(node_semver::Range::parse(version_without_equals).unwrap())
2427
});
2528
Specifier::Exact(Self {
2629
raw: raw.to_string(),

src/specifier/parser.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ pub fn is_simple_semver(str: &str) -> bool {
55
}
66

77
pub fn is_exact(str: &str) -> bool {
8-
regexes::EXACT.is_match(str) || regexes::EXACT_TAG.is_match(str)
8+
regexes::EXACT.is_match(str)
9+
|| regexes::EXACT_TAG.is_match(str)
10+
|| regexes::EXACT_EQUALS.is_match(str)
11+
|| regexes::EXACT_EQUALS_TAG.is_match(str)
912
}
1013

1114
pub fn is_latest(str: &str) -> bool {

src/specifier/regexes.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ lazy_static! {
77
pub static ref EXACT: Regex = Regex::new(r"^[0-9]+\.[0-9]+\.[0-9]+$").unwrap();
88
/// "1.2.3-alpha" || "1.2.3-rc.1"
99
pub static ref EXACT_TAG: Regex = Regex::new(r"^[0-9]+\.[0-9]+\.[0-9]+\-[a-z0-9.-_]+$").unwrap();
10+
/// "=1.2.3"
11+
pub static ref EXACT_EQUALS: Regex = Regex::new(r"^=([0-9]+\.[0-9]+\.[0-9]+)$").unwrap();
12+
/// "=1.2.3-alpha" || "=1.2.3-rc.1"
13+
pub static ref EXACT_EQUALS_TAG: Regex = Regex::new(r"^=([0-9]+\.[0-9]+\.[0-9]+)\-[a-z0-9.-_]+$").unwrap();
1014
/// "^1.2.3"
1115
pub static ref CARET: Regex = Regex::new(r"^\^([0-9]+\.[0-9]+\.[0-9]+)$").unwrap();
1216
/// "^1.2.3-alpha" || "^1.2.3-rc.1"

src/specifier_test.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,3 +822,35 @@ fn issue_213_git_tags_starting_with_v() {
822822
_ => panic!("Expected Git"),
823823
};
824824
}
825+
826+
#[test]
827+
fn parses_equals_prefix_as_exact_version() {
828+
// Test issue #239 - npm allows =X.Y.Z as equivalent to X.Y.Z
829+
let spec = Specifier::new("=9.0.0");
830+
match spec.as_ref() {
831+
Specifier::Exact(actual) => {
832+
assert_eq!(actual.raw, "=9.0.0");
833+
assert_eq!(actual.node_version.major, 9);
834+
assert_eq!(actual.node_version.minor, 0);
835+
assert_eq!(actual.node_version.patch, 0);
836+
assert_eq!(spec.get_semver_range(), Some(SemverRange::Exact));
837+
assert_eq!(spec.get_node_version().unwrap().to_string(), "9.0.0");
838+
}
839+
_ => panic!("Expected Exact, got {:?}", spec),
840+
}
841+
}
842+
843+
#[test]
844+
fn parses_equals_prefix_with_prerelease() {
845+
let spec = Specifier::new("=1.2.3-alpha.1");
846+
match spec.as_ref() {
847+
Specifier::Exact(actual) => {
848+
assert_eq!(actual.raw, "=1.2.3-alpha.1");
849+
assert_eq!(actual.node_version.major, 1);
850+
assert_eq!(actual.node_version.minor, 2);
851+
assert_eq!(actual.node_version.patch, 3);
852+
assert_eq!(spec.get_semver_range(), Some(SemverRange::Exact));
853+
}
854+
_ => panic!("Expected Exact, got {:?}", spec),
855+
}
856+
}

src/visit_packages/preferred_semver_test.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2028,4 +2028,83 @@ mod custom_types {
20282028
},
20292029
]);
20302030
}
2031+
2032+
#[test]
2033+
fn exact_version_with_equals_prefix_should_be_fixable_when_semver_group_requires_caret() {
2034+
// Reproduces issue #239
2035+
// User has semverGroups with range "^" but dependency uses =9.0.0 (npm's equals prefix)
2036+
// This should be marked as fixable (needs ^ prefix added)
2037+
let ctx = TestBuilder::new()
2038+
.with_packages(vec![json!({
2039+
"name": "pkg-a",
2040+
"version": "1.0.0",
2041+
"dependencies": {
2042+
"react": "=9.0.0"
2043+
}
2044+
})])
2045+
.with_config(json!({
2046+
"semverGroups": [{
2047+
"range": "^"
2048+
}]
2049+
}))
2050+
.build_and_visit_packages();
2051+
2052+
expect(&ctx).to_have_instances(vec![
2053+
ExpectedInstance {
2054+
state: InstanceState::valid(IsLocalAndValid),
2055+
dependency_name: "pkg-a",
2056+
id: "pkg-a in /version of pkg-a",
2057+
actual: "1.0.0",
2058+
expected: Some("1.0.0"),
2059+
overridden: None,
2060+
},
2061+
ExpectedInstance {
2062+
state: InstanceState::fixable(SemverRangeMismatch),
2063+
dependency_name: "react",
2064+
id: "react in /dependencies of pkg-a",
2065+
actual: "=9.0.0",
2066+
expected: Some("^9.0.0"),
2067+
overridden: None,
2068+
},
2069+
]);
2070+
}
2071+
2072+
#[test]
2073+
fn exact_version_without_prefix_should_be_fixable_when_semver_group_requires_caret() {
2074+
// Also test plain exact versions like "9.0.0" (without =)
2075+
// These should also be flagged when semver group requires ^
2076+
let ctx = TestBuilder::new()
2077+
.with_packages(vec![json!({
2078+
"name": "pkg-a",
2079+
"version": "1.0.0",
2080+
"dependencies": {
2081+
"react": "9.0.0"
2082+
}
2083+
})])
2084+
.with_config(json!({
2085+
"semverGroups": [{
2086+
"range": "^"
2087+
}]
2088+
}))
2089+
.build_and_visit_packages();
2090+
2091+
expect(&ctx).to_have_instances(vec![
2092+
ExpectedInstance {
2093+
state: InstanceState::valid(IsLocalAndValid),
2094+
dependency_name: "pkg-a",
2095+
id: "pkg-a in /version of pkg-a",
2096+
actual: "1.0.0",
2097+
expected: Some("1.0.0"),
2098+
overridden: None,
2099+
},
2100+
ExpectedInstance {
2101+
state: InstanceState::fixable(SemverRangeMismatch),
2102+
dependency_name: "react",
2103+
id: "react in /dependencies of pkg-a",
2104+
actual: "9.0.0",
2105+
expected: Some("^9.0.0"),
2106+
overridden: None,
2107+
},
2108+
]);
2109+
}
20312110
}

0 commit comments

Comments
 (0)