diff --git a/crates/uv-pep440/src/version_specifier.rs b/crates/uv-pep440/src/version_specifier.rs index aff917179e1f0..155585fb2cee2 100644 --- a/crates/uv-pep440/src/version_specifier.rs +++ b/crates/uv-pep440/src/version_specifier.rs @@ -573,7 +573,10 @@ impl VersionSpecifier { .version .release() .iter() - .zip(&*other.release()) + // Pad the version with zeros if it's shorter than the specifier + // prefix, e.g., version "2" (== "2.0") should NOT match "==2.1.*" + // because 2.0 != 2.1. + .zip(other.release().iter().chain(std::iter::repeat(&0))) .all(|(this, other)| this == other) } #[allow(deprecated)] @@ -590,7 +593,10 @@ impl VersionSpecifier { || !this .release() .iter() - .zip(&*version.release()) + // Pad the version with zeros if it's shorter than the specifier + // prefix, e.g., version "2" (== "2.0") should match "!=2.1.*" + // because 2.0 != 2.1. + .zip(other.release().iter().chain(std::iter::repeat(&0))) .all(|(this, other)| this == other) } Operator::TildeEqual => { @@ -1272,6 +1278,58 @@ mod tests { ); } + #[test] + fn test_equal_star_short_version_bug() { + // Version "2" (equivalent to 2.0) should NOT match "==2.1.*" + let specifier = VersionSpecifier::from_str("==2.1.*").unwrap(); + let version = Version::from_str("2").unwrap(); + assert!( + !specifier.contains(&version), + "Bug: version '2' incorrectly matches '==2.1.*'" + ); + + // Version "2" (equivalent to 2.0) SHOULD match "!=2.1.*" + let specifier = VersionSpecifier::from_str("!=2.1.*").unwrap(); + let version = Version::from_str("2").unwrap(); + assert!( + specifier.contains(&version), + "Bug: version '2' should match '!=2.1.*' (2.0 is not in 2.1 family)" + ); + + // Verify existing behavior still works: "2" matches "==2.0.*" + let specifier = VersionSpecifier::from_str("==2.0.*").unwrap(); + let version = Version::from_str("2").unwrap(); + assert!( + specifier.contains(&version), + "version '2' should match '==2.0.*'" + ); + + // And "2" should NOT match "!=2.0.*" + let specifier = VersionSpecifier::from_str("!=2.0.*").unwrap(); + let version = Version::from_str("2").unwrap(); + assert!( + !specifier.contains(&version), + "version '2' should not match '!=2.0.*'" + ); + + // Local versions: local segment should be ignored for prefix matching. + // "2+local" (== "2.0") should NOT match "==2.1.*" + let specifier = VersionSpecifier::from_str("==2.1.*").unwrap(); + let version = Version::from_str("2+local").unwrap(); + assert!( + !specifier.contains(&version), + "version '2+local' should not match '==2.1.*'" + ); + + // "2+local" (== "2.0") SHOULD match "!=2.1.*" + let specifier = VersionSpecifier::from_str("!=2.1.*").unwrap(); + let version = Version::from_str("2+local").unwrap(); + assert!( + specifier.contains(&version), + "version '2+local' should match '!=2.1.*'" + ); + } + #[test] fn test_specifiers_true() { let pairs = [