1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-28 18:19:08 -05:00
denoland-deno/cli/npm/version_req.rs
2022-08-23 10:39:19 -04:00

219 lines
6.1 KiB
Rust

// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
use std::borrow::Cow;
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use once_cell::sync::Lazy;
use regex::Regex;
use super::resolution::NpmVersionMatcher;
static MINOR_SPECIFIER_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"^[0-9]+\.[0-9]+$"#).unwrap());
/// Version requirement found in npm specifiers.
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct SpecifierVersionReq(semver::VersionReq);
impl std::fmt::Display for SpecifierVersionReq {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl SpecifierVersionReq {
// in order to keep using semver, we do some pre-processing to change the behavior
pub fn parse(text: &str) -> Result<Self, AnyError> {
// for now, we don't support these scenarios
if text.contains("||") {
bail!("not supported '||'");
}
if text.contains(',') {
bail!("not supported ','");
}
// force exact versions to be matched exactly
let text = if semver::Version::parse(text).is_ok() {
Cow::Owned(format!("={}", text))
} else {
Cow::Borrowed(text)
};
// force requirements like 1.2 to be ~1.2 instead of ^1.2
let text = if MINOR_SPECIFIER_RE.is_match(&text) {
Cow::Owned(format!("~{}", text))
} else {
text
};
Ok(Self(semver::VersionReq::parse(&text)?))
}
pub fn matches(&self, version: &semver::Version) -> bool {
self.0.matches(version)
}
}
/// A version requirement found in an npm package's dependencies.
pub struct NpmVersionReq {
raw_text: String,
comparators: Vec<semver::VersionReq>,
}
impl NpmVersionReq {
pub fn parse(text: &str) -> Result<NpmVersionReq, AnyError> {
// semver::VersionReq doesn't support spaces between comparators
// and it doesn't support using || for "OR", so we pre-process
// the version requirement in order to make this work.
let raw_text = text.to_string();
let part_texts = text.split("||").collect::<Vec<_>>();
let mut comparators = Vec::with_capacity(part_texts.len());
for part in part_texts {
comparators.push(npm_version_req_parse_part(part)?);
}
Ok(NpmVersionReq {
raw_text,
comparators,
})
}
}
impl NpmVersionMatcher for NpmVersionReq {
fn matches(&self, version: &semver::Version) -> bool {
self.comparators.iter().any(|c| c.matches(version))
}
fn version_text(&self) -> String {
self.raw_text.to_string()
}
}
fn npm_version_req_parse_part(
text: &str,
) -> Result<semver::VersionReq, AnyError> {
let text = text.trim();
let text = text.strip_prefix('v').unwrap_or(text);
// force exact versions to be matched exactly
let text = if semver::Version::parse(text).is_ok() {
Cow::Owned(format!("={}", text))
} else {
Cow::Borrowed(text)
};
// force requirements like 1.2 to be ~1.2 instead of ^1.2
let text = if MINOR_SPECIFIER_RE.is_match(&text) {
Cow::Owned(format!("~{}", text))
} else {
text
};
let mut chars = text.chars().enumerate().peekable();
let mut final_text = String::new();
while chars.peek().is_some() {
let (i, c) = chars.next().unwrap();
let is_greater_or_less_than = c == '<' || c == '>';
if is_greater_or_less_than || c == '=' {
if i > 0 {
final_text = final_text.trim().to_string();
// add a comma to make semver::VersionReq parse this
final_text.push(',');
}
final_text.push(c);
let next_char = chars.peek().map(|(_, c)| c);
if is_greater_or_less_than && matches!(next_char, Some('=')) {
let c = chars.next().unwrap().1; // skip
final_text.push(c);
}
} else {
final_text.push(c);
}
}
Ok(semver::VersionReq::parse(&final_text)?)
}
#[cfg(test)]
mod tests {
use super::*;
struct VersionReqTester(SpecifierVersionReq);
impl VersionReqTester {
fn new(text: &str) -> Self {
Self(SpecifierVersionReq::parse(text).unwrap())
}
fn matches(&self, version: &str) -> bool {
self.0.matches(&semver::Version::parse(version).unwrap())
}
}
#[test]
fn version_req_exact() {
let tester = VersionReqTester::new("1.0.1");
assert!(!tester.matches("1.0.0"));
assert!(tester.matches("1.0.1"));
assert!(!tester.matches("1.0.2"));
assert!(!tester.matches("1.1.1"));
}
#[test]
fn version_req_minor() {
let tester = VersionReqTester::new("1.1");
assert!(!tester.matches("1.0.0"));
assert!(tester.matches("1.1.0"));
assert!(tester.matches("1.1.1"));
assert!(!tester.matches("1.2.0"));
assert!(!tester.matches("1.2.1"));
}
struct NpmVersionReqTester(NpmVersionReq);
impl NpmVersionReqTester {
fn new(text: &str) -> Self {
Self(NpmVersionReq::parse(text).unwrap())
}
fn matches(&self, version: &str) -> bool {
self.0.matches(&semver::Version::parse(version).unwrap())
}
}
#[test]
pub fn npm_version_req_with_v() {
assert!(NpmVersionReq::parse("v1.0.0").is_ok());
}
#[test]
pub fn npm_version_req_exact() {
let tester = NpmVersionReqTester::new("2.1.2");
assert!(!tester.matches("2.1.1"));
assert!(tester.matches("2.1.2"));
assert!(!tester.matches("2.1.3"));
let tester = NpmVersionReqTester::new("2.1.2 || 2.1.5");
assert!(!tester.matches("2.1.1"));
assert!(tester.matches("2.1.2"));
assert!(!tester.matches("2.1.3"));
assert!(!tester.matches("2.1.4"));
assert!(tester.matches("2.1.5"));
assert!(!tester.matches("2.1.6"));
}
#[test]
pub fn npm_version_req_minor() {
let tester = NpmVersionReqTester::new("1.1");
assert!(!tester.matches("1.0.0"));
assert!(tester.matches("1.1.0"));
assert!(tester.matches("1.1.1"));
assert!(!tester.matches("1.2.0"));
assert!(!tester.matches("1.2.1"));
}
#[test]
pub fn npm_version_req_ranges() {
let tester = NpmVersionReqTester::new(">= 2.1.2 < 3.0.0 || 5.x");
assert!(!tester.matches("2.1.1"));
assert!(tester.matches("2.1.2"));
assert!(tester.matches("2.9.9"));
assert!(!tester.matches("3.0.0"));
assert!(tester.matches("5.0.0"));
assert!(tester.matches("5.1.0"));
assert!(!tester.matches("6.1.0"));
}
}