deno_semver 0.10.1

Semver for Deno
Documentation
// Copyright 2018-2025 the Deno authors. All rights reserved. MIT license.

use monch::*;
use thiserror::Error;

use crate::CowVec;
use crate::PackageTag;
use crate::RangeSetOrTag;
use crate::VersionPreOrBuild;
use crate::VersionReq;
use crate::range::Partial;
use crate::range::VersionRange;
use crate::range::VersionRangeSet;
use crate::range::XRange;

use crate::is_valid_tag;

#[derive(Error, Debug, Clone, deno_error::JsError, PartialEq, Eq)]
#[class(type)]
#[error("Invalid specifier version requirement")]
pub struct VersionReqSpecifierParseError {
  #[source]
  pub source: ParseErrorFailureError,
}

pub fn parse_version_req_from_specifier(
  text: &str,
) -> Result<VersionReq, VersionReqSpecifierParseError> {
  with_failure_handling(|input| {
    map_res(version_range, |result| {
      let (new_input, range_result) = match result {
        Ok((input, range)) => (input, Ok(range)),
        // use an empty string because we'll consider it a tag
        Err(err) => ("", Err(err)),
      };
      Ok((
        new_input,
        VersionReq::from_raw_text_and_inner(
          crate::SmallStackString::from_str(input),
          match range_result {
            Ok(range) => {
              RangeSetOrTag::RangeSet(VersionRangeSet(CowVec::from([range])))
            }
            Err(err) => {
              if is_valid_tag(input) {
                RangeSetOrTag::Tag(PackageTag::from_str(input))
              } else if input.trim().is_empty() {
                return ParseError::fail(input, "Empty version constraint.");
              } else {
                return Err(err);
              }
            }
          },
        ),
      ))
    })(input)
  })(text)
  .map_err(|err| VersionReqSpecifierParseError { source: err })
}

// Note: Although the code below looks very similar to what's used for
// parsing npm version requirements, the code here is more strict
// in order to not allow for people to get ridiculous when using
// npm/deno specifiers.
//
// A lot of the code below is adapted from https://github.com/npm/node-semver
// which is Copyright (c) Isaac Z. Schlueter and Contributors (ISC License)

// version_range ::= partial | tilde | caret
fn version_range(input: &str) -> ParseResult<'_, VersionRange> {
  or3(
    map(preceded(ch('~'), partial), |partial| {
      partial.as_tilde_version_range()
    }),
    map(preceded(ch('^'), partial), |partial| {
      partial.as_caret_version_range()
    }),
    map(partial, |partial| partial.as_equal_range()),
  )(input)
}

// partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )?
fn partial(input: &str) -> ParseResult<'_, Partial> {
  let (input, major) = xr()(input)?;
  let (input, maybe_minor) = maybe(preceded(ch('.'), xr()))(input)?;
  let (input, maybe_patch) = if maybe_minor.is_some() {
    maybe(preceded(ch('.'), xr()))(input)?
  } else {
    (input, None)
  };
  let (input, qual) = if maybe_patch.is_some() {
    maybe(qualifier)(input)?
  } else {
    (input, None)
  };
  let qual = qual.unwrap_or_default();
  Ok((
    input,
    Partial {
      major,
      minor: maybe_minor.unwrap_or(XRange::Wildcard),
      patch: maybe_patch.unwrap_or(XRange::Wildcard),
      pre: qual.pre,
      build: qual.build,
    },
  ))
}

// xr ::= 'x' | 'X' | '*' | nr
fn xr<'a>() -> impl Fn(&'a str) -> ParseResult<'a, XRange> {
  or(
    map(or3(tag("x"), tag("X"), tag("*")), |_| XRange::Wildcard),
    map(nr, XRange::Val),
  )
}

// nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) *
fn nr(input: &str) -> ParseResult<'_, u64> {
  or(map(tag("0"), |_| 0), move |input| {
    let (input, result) = if_not_empty(substring(pair(
      if_true(next_char, |c| c.is_ascii_digit() && *c != '0'),
      skip_while_byte(|b| b.is_ascii_digit()),
    )))(input)?;
    let val = match result.parse::<u64>() {
      Ok(val) => val,
      Err(err) => {
        return ParseError::fail(
          input,
          format!("Error parsing '{result}' to u64.\n\n{err:#}"),
        );
      }
    };
    Ok((input, val))
  })(input)
}

#[derive(Debug, Clone, Default)]
struct Qualifier {
  pre: CowVec<VersionPreOrBuild>,
  build: CowVec<VersionPreOrBuild>,
}

// qualifier ::= ( '-' pre )? ( '+' build )?
fn qualifier(input: &str) -> ParseResult<'_, Qualifier> {
  let (input, pre_parts) = maybe(pre)(input)?;
  let (input, build_parts) = maybe(build)(input)?;
  Ok((
    input,
    Qualifier {
      pre: pre_parts.unwrap_or_default(),
      build: build_parts.unwrap_or_default(),
    },
  ))
}

// pre ::= parts
fn pre(input: &str) -> ParseResult<'_, CowVec<VersionPreOrBuild>> {
  preceded(ch('-'), parts)(input)
}

// build ::= parts
fn build(input: &str) -> ParseResult<'_, CowVec<VersionPreOrBuild>> {
  preceded(ch('+'), parts)(input)
}

// parts ::= part ( '.' part ) *
fn parts(input: &str) -> ParseResult<'_, CowVec<VersionPreOrBuild>> {
  let (rest, parts) = separated_fold(
    part,
    ch('.'),
    CowVec::<VersionPreOrBuild>::new(),
    |mut acc, s| {
      acc.push(VersionPreOrBuild::from_str(s));
      acc
    },
  )(input)?;
  if parts.is_empty() {
    return ParseError::backtrace();
  }
  Ok((rest, parts))
}

// part ::= nr | [-0-9A-Za-z]+
fn part(input: &str) -> ParseResult<'_, &str> {
  // nr is in the other set, so don't bother checking for it
  let (rest, result) =
    take_while_byte(|b| b.is_ascii_alphanumeric() || b == b'-')(input)?;
  if result.is_empty() {
    return ParseError::backtrace();
  }
  Ok((rest, result))
}

#[cfg(test)]
mod tests {
  use super::super::Version;
  use super::*;

  struct VersionReqTester(VersionReq);

  impl VersionReqTester {
    fn new(text: &str) -> Self {
      Self(parse_version_req_from_specifier(text).unwrap())
    }

    fn matches(&self, version: &str) -> bool {
      self.0.matches(&Version::parse_from_npm(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"));

    // pre-release
    let tester = VersionReqTester::new("1.0.0-alpha.13");
    assert!(tester.matches("1.0.0-alpha.13"));
  }

  #[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"));
  }

  #[test]
  fn version_req_caret() {
    let tester = VersionReqTester::new("^1.1.1");
    assert!(!tester.matches("1.1.0"));
    assert!(tester.matches("1.1.1"));
    assert!(tester.matches("1.1.2"));
    assert!(tester.matches("1.2.0"));
    assert!(!tester.matches("2.0.0"));

    let tester = VersionReqTester::new("^0.1.1");
    assert!(!tester.matches("0.0.0"));
    assert!(!tester.matches("0.1.0"));
    assert!(tester.matches("0.1.1"));
    assert!(tester.matches("0.1.2"));
    assert!(!tester.matches("0.2.0"));
    assert!(!tester.matches("1.0.0"));

    let tester = VersionReqTester::new("^0.0.1");
    assert!(!tester.matches("0.0.0"));
    assert!(tester.matches("0.0.1"));
    assert!(!tester.matches("0.0.2"));
    assert!(!tester.matches("0.1.0"));
    assert!(!tester.matches("1.0.0"));
  }

  #[test]
  fn version_req_tilde() {
    let tester = VersionReqTester::new("~1.1.1");
    assert!(!tester.matches("1.1.0"));
    assert!(tester.matches("1.1.1"));
    assert!(tester.matches("1.1.2"));
    assert!(!tester.matches("1.2.0"));
    assert!(!tester.matches("2.0.0"));

    let tester = VersionReqTester::new("~0.1.1");
    assert!(!tester.matches("0.0.0"));
    assert!(!tester.matches("0.1.0"));
    assert!(tester.matches("0.1.1"));
    assert!(tester.matches("0.1.2"));
    assert!(!tester.matches("0.2.0"));
    assert!(!tester.matches("1.0.0"));

    let tester = VersionReqTester::new("~0.0.1");
    assert!(!tester.matches("0.0.0"));
    assert!(tester.matches("0.0.1"));
    assert!(tester.matches("0.0.2")); // for some reason this matches, but not with ^
    assert!(!tester.matches("0.1.0"));
    assert!(!tester.matches("1.0.0"));
  }

  #[test]
  fn version_req_pre_release() {
    let tester = VersionReqTester::new("^1.0.1-pre-release");
    assert!(!tester.matches("1.0.0"));
    assert!(tester.matches("1.0.1"));
    assert!(tester.matches("1.0.1-pre-release"));

    // zero version
    let tester = VersionReqTester::new("^0.0.0-pre-release");
    assert!(tester.matches("0.0.0"));
    assert!(!tester.matches("0.0.1"));
    assert!(tester.matches("0.0.0-pre-release"));
  }

  #[test]
  fn parses_tag() {
    let latest_tag = VersionReq::parse_from_specifier("latest").unwrap();
    assert_eq!(latest_tag.tag().unwrap(), "latest");
  }
}