summaryrefslogtreecommitdiff
path: root/cli/npm/version_req.rs
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2022-08-23 10:39:19 -0400
committerGitHub <noreply@github.com>2022-08-23 10:39:19 -0400
commite7367044d933ee3518ae583a43876a0ddab5b17e (patch)
treee557e3ea4faad632bcfafa6539f3bd0115dc6426 /cli/npm/version_req.rs
parent362af63c6f45e98948536d08d2d6195af87f729c (diff)
feat: binary npm commands (#15542)
Diffstat (limited to 'cli/npm/version_req.rs')
-rw-r--r--cli/npm/version_req.rs219
1 files changed, 219 insertions, 0 deletions
diff --git a/cli/npm/version_req.rs b/cli/npm/version_req.rs
new file mode 100644
index 000000000..24f3788ad
--- /dev/null
+++ b/cli/npm/version_req.rs
@@ -0,0 +1,219 @@
+// 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"));
+ }
+}