From e3446bfc0c91ac42f49da73be3e723b350843fb6 Mon Sep 17 00:00:00 2001
From: Fedor Lukyanov <sleekybadger@gmail.com>
Date: Thu, 3 Jan 2019 07:32:08 +0200
Subject: [PATCH] feat: Ruby semver versioning (#3000)

---
 lib/versioning/semver-ruby/index.js           |  83 ++++
 lib/versioning/semver-ruby/operator.js        |  27 ++
 lib/versioning/semver-ruby/range.js           |  49 +++
 lib/versioning/semver-ruby/strategies/bump.js |  32 ++
 .../semver-ruby/strategies/index.js           |   9 +
 lib/versioning/semver-ruby/strategies/pin.js  |   1 +
 .../semver-ruby/strategies/replace.js         |   5 +
 lib/versioning/semver-ruby/version.js         | 100 +++++
 package.json                                  |   3 +-
 test/versioning/semver-ruby.spec.js           | 412 ++++++++++++++++++
 yarn.lock                                     |  18 +-
 11 files changed, 728 insertions(+), 11 deletions(-)
 create mode 100644 lib/versioning/semver-ruby/index.js
 create mode 100644 lib/versioning/semver-ruby/operator.js
 create mode 100644 lib/versioning/semver-ruby/range.js
 create mode 100644 lib/versioning/semver-ruby/strategies/bump.js
 create mode 100644 lib/versioning/semver-ruby/strategies/index.js
 create mode 100644 lib/versioning/semver-ruby/strategies/pin.js
 create mode 100644 lib/versioning/semver-ruby/strategies/replace.js
 create mode 100644 lib/versioning/semver-ruby/version.js
 create mode 100644 test/versioning/semver-ruby.spec.js

diff --git a/lib/versioning/semver-ruby/index.js b/lib/versioning/semver-ruby/index.js
new file mode 100644
index 0000000000..310817d04d
--- /dev/null
+++ b/lib/versioning/semver-ruby/index.js
@@ -0,0 +1,83 @@
+const {
+  eq,
+  valid,
+  gt,
+  satisfies,
+  maxSatisfying,
+  minSatisfying,
+} = require('@snyk/ruby-semver');
+const { parse: parseVersion } = require('./version');
+const { parse: parseRange, ltr } = require('./range');
+const { isSingleOperator, isValidOperator } = require('./operator');
+const { pin, bump, replace } = require('./strategies');
+
+const equals = (left, right) => eq(left, right);
+
+const getMajor = version => parseVersion(version).major;
+const getMinor = version => parseVersion(version).minor;
+const getPatch = version => parseVersion(version).patch;
+
+const isVersion = version => !!valid(version);
+const isGreaterThan = (left, right) => gt(left, right);
+const isLessThanRange = (version, range) => ltr(version, range);
+
+const isSingleVersion = range => {
+  const { version, operator } = parseRange(range);
+
+  return operator
+    ? isVersion(version) && isSingleOperator(operator)
+    : isVersion(version);
+};
+
+const isStable = version =>
+  parseVersion(version).prerelease ? false : isVersion(version);
+
+const isValid = range => {
+  const { version, operator } = parseRange(range);
+
+  return operator
+    ? isVersion(version) && isValidOperator(operator)
+    : isVersion(version);
+};
+
+const matches = (version, range) => satisfies(version, range);
+const maxSatisfyingVersion = (versions, range) =>
+  maxSatisfying(versions, range);
+const minSatisfyingVersion = (versions, range) =>
+  minSatisfying(versions, range);
+
+const getNewValue = (currentValue, rangeStrategy, fromVersion, toVersion) => {
+  switch (rangeStrategy) {
+    case 'pin':
+      return pin({ to: toVersion });
+    case 'bump':
+      return bump({ range: currentValue, to: toVersion });
+    case 'replace':
+      return replace({ range: currentValue, to: toVersion });
+    // istanbul ignore next
+    default:
+      logger.warn(`Unsupported strategy ${rangeStrategy}`);
+      return null;
+  }
+};
+
+const sortVersions = (left, right) => gt(left, right);
+
+module.exports = {
+  equals,
+  getMajor,
+  getMinor,
+  getPatch,
+  isCompatible: isVersion,
+  isGreaterThan,
+  isLessThanRange,
+  isSingleVersion,
+  isStable,
+  isValid,
+  isVersion,
+  matches,
+  maxSatisfyingVersion,
+  minSatisfyingVersion,
+  getNewValue,
+  sortVersions,
+};
diff --git a/lib/versioning/semver-ruby/operator.js b/lib/versioning/semver-ruby/operator.js
new file mode 100644
index 0000000000..9a85b898ba
--- /dev/null
+++ b/lib/versioning/semver-ruby/operator.js
@@ -0,0 +1,27 @@
+const EQUAL = '=';
+const NOT_EQUAL = '!=';
+
+const GT = '>';
+const LT = '<';
+
+const GTE = '>=';
+const LTE = '<=';
+const PGTE = '~>';
+
+const SINGLE = [EQUAL];
+const ALL = [EQUAL, NOT_EQUAL, GT, LT, GTE, LTE, PGTE];
+
+const isValidOperator = operator => ALL.includes(operator);
+const isSingleOperator = operator => SINGLE.includes(operator);
+
+module.exports = {
+  EQUAL,
+  NOT_EQUAL,
+  GT,
+  LT,
+  GTE,
+  LTE,
+  PGTE,
+  isValidOperator,
+  isSingleOperator,
+};
diff --git a/lib/versioning/semver-ruby/range.js b/lib/versioning/semver-ruby/range.js
new file mode 100644
index 0000000000..7aa8b6a6cf
--- /dev/null
+++ b/lib/versioning/semver-ruby/range.js
@@ -0,0 +1,49 @@
+const GemVersion = require('@snyk/ruby-semver/lib/ruby/gem-version');
+const GemRequirement = require('@snyk/ruby-semver/lib/ruby/gem-requirement');
+const { EQUAL, NOT_EQUAL, GT, LT, GTE, LTE, PGTE } = require('./operator');
+
+const parse = range => {
+  const regExp = /^([^\d\s]+)?\s?([0-9a-zA-Z-.-]+)$/g;
+
+  const value = (range || '').trim();
+  const matches = regExp.exec(value) || {};
+
+  return {
+    version: matches[2] || null,
+    operator: matches[1] || null,
+  };
+};
+
+const ltr = (version, range) => {
+  const gemVersion = GemVersion.create(version);
+  const requirements = range.split(',').map(GemRequirement.parse);
+
+  const results = requirements.map(([operator, ver]) => {
+    switch (operator) {
+      case GT:
+      case LT:
+        return gemVersion.compare(ver) <= 0;
+      case GTE:
+      case LTE:
+      case EQUAL:
+      case NOT_EQUAL:
+        return gemVersion.compare(ver) < 0;
+      case PGTE:
+        return (
+          gemVersion.compare(ver) < 0 &&
+          gemVersion.release().compare(ver.bump()) <= 0
+        );
+      // istanbul ignore next
+      default:
+        logger.warn(`Unsupported operator '${operator}'`);
+        return null;
+    }
+  });
+
+  return results.reduce((accumulator, value) => accumulator && value, true);
+};
+
+module.exports = {
+  parse,
+  ltr,
+};
diff --git a/lib/versioning/semver-ruby/strategies/bump.js b/lib/versioning/semver-ruby/strategies/bump.js
new file mode 100644
index 0000000000..28fe77f4c9
--- /dev/null
+++ b/lib/versioning/semver-ruby/strategies/bump.js
@@ -0,0 +1,32 @@
+const { gte, lte } = require('@snyk/ruby-semver');
+const { EQUAL, NOT_EQUAL, GT, LT, GTE, LTE, PGTE } = require('../operator');
+const { floor, increment, decrement } = require('../version');
+const { parse: parseRange } = require('../range');
+
+module.exports = ({ range, to }) => {
+  const ranges = range.split(',').map(parseRange);
+  const results = ranges.map(({ operator, version: ver }) => {
+    switch (operator) {
+      case null:
+        return to;
+      case GT:
+        return lte(to, ver) ? `${GT} ${ver}` : `${GT} ${decrement(to)}`;
+      case LT:
+        return gte(to, ver) ? `${LT} ${increment(ver, to)}` : `${LT} ${ver}`;
+      case PGTE:
+        return `${operator} ${floor(to)}`;
+      case GTE:
+      case LTE:
+      case EQUAL:
+        return `${operator} ${to}`;
+      case NOT_EQUAL:
+        return `${NOT_EQUAL} ${ver}`;
+      // istanbul ignore next
+      default:
+        logger.warn(`Unsupported operator '${operator}'`);
+        return null;
+    }
+  });
+
+  return results.join(', ');
+};
diff --git a/lib/versioning/semver-ruby/strategies/index.js b/lib/versioning/semver-ruby/strategies/index.js
new file mode 100644
index 0000000000..a1bc4517ce
--- /dev/null
+++ b/lib/versioning/semver-ruby/strategies/index.js
@@ -0,0 +1,9 @@
+const pin = require('./pin');
+const bump = require('./bump');
+const replace = require('./replace');
+
+module.exports = {
+  pin,
+  bump,
+  replace,
+};
diff --git a/lib/versioning/semver-ruby/strategies/pin.js b/lib/versioning/semver-ruby/strategies/pin.js
new file mode 100644
index 0000000000..071862075c
--- /dev/null
+++ b/lib/versioning/semver-ruby/strategies/pin.js
@@ -0,0 +1 @@
+module.exports = ({ to }) => to;
diff --git a/lib/versioning/semver-ruby/strategies/replace.js b/lib/versioning/semver-ruby/strategies/replace.js
new file mode 100644
index 0000000000..02bffe5664
--- /dev/null
+++ b/lib/versioning/semver-ruby/strategies/replace.js
@@ -0,0 +1,5 @@
+const { satisfies } = require('@snyk/ruby-semver');
+const bump = require('./bump');
+
+module.exports = ({ to, range }) =>
+  satisfies(to, range) ? range : bump({ to, range });
diff --git a/lib/versioning/semver-ruby/version.js b/lib/versioning/semver-ruby/version.js
new file mode 100644
index 0000000000..3fcc9c680b
--- /dev/null
+++ b/lib/versioning/semver-ruby/version.js
@@ -0,0 +1,100 @@
+const last = require('lodash/last');
+const GemVersion = require('@snyk/ruby-semver/lib/ruby/gem-version');
+const { diff, major, minor, patch, prerelease } = require('@snyk/ruby-semver');
+
+const parse = version => ({
+  major: major(version),
+  minor: minor(version),
+  patch: patch(version),
+  prerelease: prerelease(version),
+});
+
+const adapt = (left, right) =>
+  left
+    .split('.')
+    .slice(0, right.split('.').length)
+    .join('.');
+
+const floor = version =>
+  [
+    ...GemVersion.create(version)
+      .release()
+      .getSegments()
+      .slice(0, -1),
+    0,
+  ].join('.');
+
+// istanbul ignore next
+const incrementLastSegment = version => {
+  const segments = GemVersion.create(version)
+    .release()
+    .getSegments();
+  const nextLast = parseInt(last(segments), 10) + 1;
+
+  return [...segments.slice(0, -1), nextLast].join('.');
+};
+
+// istanbul ignore next
+const incrementMajor = (maj, min, ptch, pre) =>
+  min === 0 || ptch === 0 || pre.length === 0 ? maj + 1 : maj;
+
+// istanbul ignore next
+const incrementMinor = (min, ptch, pre) =>
+  ptch === 0 || pre.length === 0 ? min + 1 : min;
+
+// istanbul ignore next
+const incrementPatch = (ptch, pre) => (pre.length === 0 ? ptch + 1 : ptch);
+
+// istanbul ignore next
+const increment = (from, to) => {
+  const { major: maj, minor: min, patch: ptch, pre } = parse(from);
+
+  let nextVersion;
+  switch (diff(from, adapt(to, from))) {
+    case 'major':
+      nextVersion = [incrementMajor(maj, min, ptch, pre || []), 0, 0].join('.');
+      break;
+    case 'minor':
+      nextVersion = [maj, incrementMinor(min, ptch, pre || []), 0].join('.');
+      break;
+    case 'patch':
+      nextVersion = [maj, min, incrementPatch(ptch, pre || [])].join('.');
+      break;
+    case 'prerelease':
+      nextVersion = [maj, min, ptch].join('.');
+      break;
+    default:
+      return incrementLastSegment(from);
+  }
+
+  return increment(nextVersion, to);
+};
+
+// istanbul ignore next
+const decrement = version => {
+  const segments = GemVersion.create(version)
+    .release()
+    .getSegments();
+  const nextSegments = segments
+    .reverse()
+    .reduce((accumulator, segment, index) => {
+      if (index === 0) {
+        return [segment - 1];
+      }
+
+      if (accumulator[index - 1] === -1) {
+        return [...accumulator.slice(0, index - 1), 0, segment - 1];
+      }
+
+      return [...accumulator, segment];
+    }, []);
+
+  return nextSegments.reverse().join('.');
+};
+
+module.exports = {
+  parse,
+  floor,
+  increment,
+  decrement,
+};
diff --git a/package.json b/package.json
index 5836fd0e22..86317c8ae8 100644
--- a/package.json
+++ b/package.json
@@ -136,7 +136,8 @@
     "upath": "1.1.0",
     "validator": "10.9.0",
     "www-authenticate": "0.6.2",
-    "yarn": "1.9.4"
+    "yarn": "1.9.4",
+    "@snyk/ruby-semver": "2.0.0"
   },
   "devDependencies": {
     "babel-plugin-transform-object-rest-spread": "6.26.0",
diff --git a/test/versioning/semver-ruby.spec.js b/test/versioning/semver-ruby.spec.js
new file mode 100644
index 0000000000..a828b0826d
--- /dev/null
+++ b/test/versioning/semver-ruby.spec.js
@@ -0,0 +1,412 @@
+const semverRuby = require('../../lib/versioning/semver-ruby');
+
+describe('semverRuby', () => {
+  describe('.equals', () => {
+    it('returns true when versions are equal', () => {
+      expect(semverRuby.equals('1.0.0', '1')).toBe(true);
+      expect(semverRuby.equals('1.2.0', '1.2')).toBe(true);
+      expect(semverRuby.equals('1.2.0', '1.2.0')).toBe(true);
+      expect(semverRuby.equals('1.0.0.rc1', '1.0.0.rc1')).toBe(true);
+    });
+
+    it('returns false when versions are different', () => {
+      expect(semverRuby.equals('1.2.0', '2')).toBe(false);
+      expect(semverRuby.equals('1.2.0', '1.1')).toBe(false);
+      expect(semverRuby.equals('1.2.0', '1.2.1')).toBe(false);
+      expect(semverRuby.equals('1.0.0.rc1', '1.0.0.rc2')).toBe(false);
+    });
+  });
+
+  describe('.getMajor', () => {
+    it('returns major segment of version', () => {
+      expect(semverRuby.getMajor('1')).toEqual(1);
+      expect(semverRuby.getMajor('1.2')).toEqual(1);
+      expect(semverRuby.getMajor('1.2.0')).toEqual(1);
+      expect(semverRuby.getMajor('1.2.0.alpha.4')).toEqual(1);
+    });
+  });
+
+  describe('.getMinor', () => {
+    it('returns minor segment of version when it present', () => {
+      expect(semverRuby.getMinor('1.2')).toEqual(2);
+      expect(semverRuby.getMinor('1.2.0')).toEqual(2);
+      expect(semverRuby.getMinor('1.2.0.alpha.4')).toEqual(2);
+    });
+
+    it('returns null when minor segment absent', () => {
+      expect(semverRuby.getMinor('1')).toEqual(null);
+    });
+  });
+
+  describe('.getPatch', () => {
+    it('returns patch segment of version when it present', () => {
+      expect(semverRuby.getPatch('1.2.2')).toEqual(2);
+      expect(semverRuby.getPatch('1.2.1.alpha.4')).toEqual(1);
+    });
+
+    it('returns null when patch segment absent', () => {
+      expect(semverRuby.getPatch('1')).toEqual(null);
+      expect(semverRuby.getPatch('1.2')).toEqual(null);
+    });
+  });
+
+  describe('.isVersion', () => {
+    it('returns true when version is valid', () => {
+      expect(semverRuby.isVersion('1')).toBeTruthy();
+      expect(semverRuby.isVersion('1.1')).toBeTruthy();
+      expect(semverRuby.isVersion('1.1.2')).toBeTruthy();
+      expect(semverRuby.isVersion('1.1.2.3')).toBeTruthy();
+      expect(semverRuby.isVersion('1.1.2-4')).toBeTruthy();
+      expect(semverRuby.isVersion('1.1.2.pre.4')).toBeTruthy();
+    });
+
+    it('returns false when version is invalid', () => {
+      expect(semverRuby.isVersion()).toBeFalsy();
+      expect(semverRuby.isVersion('')).toBeFalsy();
+      expect(semverRuby.isVersion(null)).toBeFalsy();
+      expect(semverRuby.isVersion('tottally-not-a-version')).toBeFalsy();
+    });
+  });
+
+  describe('.isGreaterThan', () => {
+    it('returns true when version is greater than another', () => {
+      expect(semverRuby.isGreaterThan('2', '1')).toBeTruthy();
+      expect(semverRuby.isGreaterThan('2.2', '2.1')).toBeTruthy();
+      expect(semverRuby.isGreaterThan('2.2.1', '2.2.0')).toBeTruthy();
+      expect(semverRuby.isGreaterThan('3.0.0.rc2', '3.0.0.rc1')).toBeTruthy();
+      expect(semverRuby.isGreaterThan('3.0.0-rc.2', '3.0.0-rc.1')).toBeTruthy();
+      expect(semverRuby.isGreaterThan('3.0.0.rc1', '3.0.0.beta')).toBeTruthy();
+      expect(semverRuby.isGreaterThan('3.0.0-rc.1', '3.0.0-beta')).toBeTruthy();
+      expect(
+        semverRuby.isGreaterThan('3.0.0.beta', '3.0.0.alpha')
+      ).toBeTruthy();
+      expect(
+        semverRuby.isGreaterThan('3.0.0-beta', '3.0.0-alpha')
+      ).toBeTruthy();
+      expect(semverRuby.isGreaterThan('5.0.1.rc1', '5.0.1.beta1')).toBeTruthy();
+      expect(
+        semverRuby.isGreaterThan('5.0.1-rc.1', '5.0.1-beta.1')
+      ).toBeTruthy();
+    });
+
+    it('returns false when version is lower than another', () => {
+      expect(semverRuby.isGreaterThan('1', '2')).toBeFalsy();
+      expect(semverRuby.isGreaterThan('2.1', '2.2')).toBeFalsy();
+      expect(semverRuby.isGreaterThan('2.2.0', '2.2.1')).toBeFalsy();
+      expect(semverRuby.isGreaterThan('3.0.0.rc1', '3.0.0.rc2')).toBeFalsy();
+      expect(semverRuby.isGreaterThan('3.0.0-rc.1', '3.0.0-rc.2')).toBeFalsy();
+      expect(semverRuby.isGreaterThan('3.0.0.beta', '3.0.0.rc1')).toBeFalsy();
+      expect(semverRuby.isGreaterThan('3.0.0-beta', '3.0.0-rc.1')).toBeFalsy();
+      expect(semverRuby.isGreaterThan('3.0.0.alpha', '3.0.0.beta')).toBeFalsy();
+      expect(semverRuby.isGreaterThan('3.0.0-alpha', '3.0.0-beta')).toBeFalsy();
+      expect(semverRuby.isGreaterThan('5.0.1.beta1', '5.0.1.rc1')).toBeFalsy();
+      expect(
+        semverRuby.isGreaterThan('5.0.1-beta.1', '5.0.1-rc.1')
+      ).toBeFalsy();
+    });
+
+    it('returns false when versions are equal', () => {
+      expect(semverRuby.isGreaterThan('1', '1')).toBeFalsy();
+      expect(semverRuby.isGreaterThan('2.1', '2.1')).toBeFalsy();
+      expect(semverRuby.isGreaterThan('2.2.0', '2.2.0')).toBeFalsy();
+      expect(semverRuby.isGreaterThan('3.0.0.rc1', '3.0.0.rc1')).toBeFalsy();
+      expect(semverRuby.isGreaterThan('3.0.0-rc.1', '3.0.0-rc.1')).toBeFalsy();
+      expect(semverRuby.isGreaterThan('3.0.0.beta', '3.0.0.beta')).toBeFalsy();
+      expect(semverRuby.isGreaterThan('3.0.0-beta', '3.0.0-beta')).toBeFalsy();
+      expect(
+        semverRuby.isGreaterThan('3.0.0.alpha', '3.0.0.alpha')
+      ).toBeFalsy();
+      expect(
+        semverRuby.isGreaterThan('3.0.0-alpha', '3.0.0-alpha')
+      ).toBeFalsy();
+      expect(
+        semverRuby.isGreaterThan('5.0.1.beta1', '5.0.1.beta1')
+      ).toBeFalsy();
+      expect(
+        semverRuby.isGreaterThan('5.0.1-beta.1', '5.0.1-beta.1')
+      ).toBeFalsy();
+    });
+  });
+
+  describe('.isStable', () => {
+    it('returns true when version is stable', () => {
+      expect(semverRuby.isStable('1')).toBeTruthy();
+      expect(semverRuby.isStable('1.2')).toBeTruthy();
+      expect(semverRuby.isStable('1.2.3')).toBeTruthy();
+    });
+
+    it('returns false when version is prerelease', () => {
+      expect(semverRuby.isStable('1.2.0-alpha')).toBeFalsy();
+      expect(semverRuby.isStable('1.2.0.alpha')).toBeFalsy();
+      expect(semverRuby.isStable('1.2.0.alpha1')).toBeFalsy();
+      expect(semverRuby.isStable('1.2.0-alpha.1')).toBeFalsy();
+    });
+
+    it('returns false when version is invalid', () => {
+      expect(semverRuby.isStable()).toBeFalsy();
+      expect(semverRuby.isStable('')).toBeFalsy();
+      expect(semverRuby.isStable(null)).toBeFalsy();
+      expect(semverRuby.isStable('tottally-not-a-version')).toBeFalsy();
+    });
+  });
+
+  describe('.sortVersions', () => {
+    it('sorts versions in an ascending order', () => {
+      expect(
+        ['1.2.3-beta', '2.0.1', '1.3.4', '1.2.3'].sort(semverRuby.sortVersions)
+      ).toEqual(['1.2.3-beta', '1.2.3', '1.3.4', '2.0.1']);
+    });
+  });
+
+  describe('.minSatisfyingVersion', () => {
+    it('returns lowest version that matches range', () => {
+      expect(
+        semverRuby.minSatisfyingVersion(['2.1.5', '2.1.6'], '~> 2.1')
+      ).toEqual('2.1.5');
+
+      expect(
+        semverRuby.minSatisfyingVersion(['2.1.6', '2.1.5'], '~> 2.1.6')
+      ).toEqual('2.1.6');
+
+      expect(
+        semverRuby.minSatisfyingVersion(
+          ['4.7.3', '4.7.4', '4.7.5', '4.7.9'],
+          '~> 4.7, >= 4.7.4'
+        )
+      ).toEqual('4.7.4');
+
+      expect(
+        semverRuby.minSatisfyingVersion(
+          ['2.5.3', '2.5.4', '2.5.5', '2.5.6'],
+          '~>2.5.3'
+        )
+      ).toEqual('2.5.3');
+
+      expect(
+        semverRuby.minSatisfyingVersion(
+          ['2.1.0', '3.0.0.beta', '2.3', '3.0.0-rc.1', '3.0.0', '3.1.1'],
+          '~> 3.0'
+        )
+      ).toEqual('3.0.0');
+    });
+
+    it('returns null if version that matches range absent', () => {
+      expect(
+        semverRuby.minSatisfyingVersion(['1.2.3', '1.2.4'], '>= 3.5.0')
+      ).toEqual(null);
+    });
+  });
+
+  describe('.maxSatisfyingVersion', () => {
+    it('returns greatest version that matches range', () => {
+      expect(
+        semverRuby.maxSatisfyingVersion(['2.1.5', '2.1.6'], '~> 2.1')
+      ).toEqual('2.1.6');
+
+      expect(
+        semverRuby.maxSatisfyingVersion(['2.1.6', '2.1.5'], '~> 2.1.6')
+      ).toEqual('2.1.6');
+
+      expect(
+        semverRuby.maxSatisfyingVersion(
+          ['4.7.3', '4.7.4', '4.7.5', '4.7.9'],
+          '~> 4.7, >= 4.7.4'
+        )
+      ).toEqual('4.7.9');
+
+      expect(
+        semverRuby.maxSatisfyingVersion(
+          ['2.5.3', '2.5.4', '2.5.5', '2.5.6'],
+          '~>2.5.3'
+        )
+      ).toEqual('2.5.6');
+
+      expect(
+        semverRuby.maxSatisfyingVersion(
+          ['2.1.0', '3.0.0.beta', '2.3', '3.0.0-rc.1', '3.0.0', '3.1.1'],
+          '~> 3.0'
+        )
+      ).toEqual('3.1.1');
+    });
+
+    it('returns null if version that matches range absent', () => {
+      expect(
+        semverRuby.maxSatisfyingVersion(['1.2.3', '1.2.4'], '>= 3.5.0')
+      ).toEqual(null);
+    });
+  });
+
+  describe('.matches', () => {
+    it('returns true when version match range', () => {
+      expect(semverRuby.matches('1.2', '>= 1.2')).toBeTruthy();
+      expect(semverRuby.matches('1.2.3', '~> 1.2.1')).toBeTruthy();
+      expect(semverRuby.matches('1.2.7', '1.2.7')).toBeTruthy();
+      expect(semverRuby.matches('1.1.6', '>= 1.1.5, < 2.0')).toBeTruthy();
+    });
+
+    it('returns false when version not match range', () => {
+      expect(semverRuby.matches('1.2', '>= 1.3')).toBeFalsy();
+      expect(semverRuby.matches('1.3.8', '~> 1.2.1')).toBeFalsy();
+      expect(semverRuby.matches('1.3.9', '1.3.8')).toBeFalsy();
+      expect(semverRuby.matches('2.0.0', '>= 1.1.5, < 2.0')).toBeFalsy();
+    });
+  });
+
+  describe('.isLessThanRange', () => {
+    it('returns true when version less than range', () => {
+      expect(semverRuby.isLessThanRange('1.2.2', '< 1.2.2')).toBeTruthy();
+      expect(
+        semverRuby.isLessThanRange('1.1.4', '>= 1.1.5, < 2.0')
+      ).toBeTruthy();
+      expect(
+        semverRuby.isLessThanRange('1.2.0-alpha', '1.2.0-beta')
+      ).toBeTruthy();
+      expect(
+        semverRuby.isLessThanRange('1.2.2', '> 1.2.2, ~> 2.0.0')
+      ).toBeTruthy();
+    });
+
+    it('returns false when version greater or satisfies range', () => {
+      expect(semverRuby.isLessThanRange('1.2.2', '<= 1.2.2')).toBeFalsy();
+      expect(
+        semverRuby.isLessThanRange('2.0.0', '>= 1.1.5, < 2.0')
+      ).toBeFalsy();
+      expect(
+        semverRuby.isLessThanRange('1.2.0-beta', '1.2.0-alpha')
+      ).toBeFalsy();
+      expect(
+        semverRuby.isLessThanRange('2.0.0', '> 1.2.2, ~> 2.0.0')
+      ).toBeFalsy();
+    });
+  });
+
+  describe('.isValid', () => {
+    it('returns true when version is valid', () => {
+      expect(semverRuby.isValid('1')).toBeTruthy();
+      expect(semverRuby.isValid('1.1')).toBeTruthy();
+      expect(semverRuby.isValid('1.1.2')).toBeTruthy();
+      expect(semverRuby.isValid('1.2.0.alpha1')).toBeTruthy();
+      expect(semverRuby.isValid('1.2.0-alpha.1')).toBeTruthy();
+
+      expect(semverRuby.isValid('= 1')).toBeTruthy();
+      expect(semverRuby.isValid('!= 1.1')).toBeTruthy();
+      expect(semverRuby.isValid('> 1.1.2')).toBeTruthy();
+      expect(semverRuby.isValid('< 1.0.0-beta')).toBeTruthy();
+      expect(semverRuby.isValid('>= 1.0.0.beta')).toBeTruthy();
+      expect(semverRuby.isValid('<= 1.2.0.alpha1')).toBeTruthy();
+      expect(semverRuby.isValid('~> 1.2.0-alpha.1')).toBeTruthy();
+    });
+
+    it('returns false when version is invalid', () => {
+      expect(semverRuby.isVersion()).toBeFalsy();
+      expect(semverRuby.isVersion('')).toBeFalsy();
+      expect(semverRuby.isVersion(null)).toBeFalsy();
+      expect(semverRuby.isVersion('tottally-not-a-version')).toBeFalsy();
+
+      expect(semverRuby.isValid('+ 1')).toBeFalsy();
+      expect(semverRuby.isValid('- 1.1')).toBeFalsy();
+      expect(semverRuby.isValid('=== 1.1.2')).toBeFalsy();
+      expect(semverRuby.isValid('! 1.0.0-beta')).toBeFalsy();
+      expect(semverRuby.isValid('& 1.0.0.beta')).toBeFalsy();
+    });
+  });
+
+  describe('.isSingleVersion', () => {
+    it('returns true when version is single', () => {
+      expect(semverRuby.isSingleVersion('1')).toBeTruthy();
+      expect(semverRuby.isSingleVersion('1.2')).toBeTruthy();
+      expect(semverRuby.isSingleVersion('1.2.1')).toBeTruthy();
+
+      expect(semverRuby.isSingleVersion('=1')).toBeTruthy();
+      expect(semverRuby.isSingleVersion('=1.2')).toBeTruthy();
+      expect(semverRuby.isSingleVersion('=1.2.1')).toBeTruthy();
+
+      expect(semverRuby.isSingleVersion('= 1')).toBeTruthy();
+      expect(semverRuby.isSingleVersion('= 1.2')).toBeTruthy();
+      expect(semverRuby.isSingleVersion('= 1.2.1')).toBeTruthy();
+
+      expect(semverRuby.isSingleVersion('1.2.1.rc1')).toBeTruthy();
+      expect(semverRuby.isSingleVersion('1.2.1-rc.1')).toBeTruthy();
+
+      expect(semverRuby.isSingleVersion('= 1.2.0.alpha')).toBeTruthy();
+      expect(semverRuby.isSingleVersion('= 1.2.0-alpha')).toBeTruthy();
+    });
+
+    it('returns false when version is multiple', () => {
+      expect(semverRuby.isSingleVersion('!= 1')).toBeFalsy();
+      expect(semverRuby.isSingleVersion('> 1.2')).toBeFalsy();
+      expect(semverRuby.isSingleVersion('< 1.2.1')).toBeFalsy();
+      expect(semverRuby.isSingleVersion('>= 1')).toBeFalsy();
+      expect(semverRuby.isSingleVersion('<= 1.2')).toBeFalsy();
+      expect(semverRuby.isSingleVersion('~> 1.2.1')).toBeFalsy();
+    });
+
+    it('returns false when version is invalid', () => {
+      expect(semverRuby.isSingleVersion()).toBeFalsy();
+      expect(semverRuby.isSingleVersion('')).toBeFalsy();
+      expect(semverRuby.isSingleVersion(null)).toBeFalsy();
+      expect(semverRuby.isSingleVersion('tottally-not-a-version')).toBeFalsy();
+    });
+  });
+
+  describe('.getNewValue', () => {
+    it('returns correct version for pin strategy', () => {
+      [
+        ['1.2.3', '1.0.3', 'pin', '1.0.3', '1.2.3'],
+        ['1.2.3', '= 1.0.3', 'pin', '1.0.3', '1.2.3'],
+        ['1.2.3', '!= 1.0.3', 'pin', '1.0.4', '1.2.3'],
+        ['1.2.3', '> 1.0.3', 'pin', '1.0.4', '1.2.3'],
+        ['1.2.3', '< 1.0.3', 'pin', '1.0.2', '1.2.3'],
+        ['1.2.3', '>= 1.0.3', 'pin', '1.0.4', '1.2.3'],
+        ['1.2.3', '<= 1.0.3', 'pin', '1.0.3', '1.2.3'],
+        ['1.2.3', '~> 1.0.3', 'pin', '1.0.4', '1.2.3'],
+        ['4.7.8', '~> 4.7, >= 4.7.4', 'pin', '4.7.5', '4.7.8'],
+      ].forEach(([expected, ...params]) => {
+        expect(semverRuby.getNewValue(...params)).toEqual(expected);
+      });
+    });
+
+    it('returns correct version for bump strategy', () => {
+      [
+        ['1.2.3', '1.0.3', 'bump', '1.0.3', '1.2.3'],
+        ['= 1.2.3', '= 1.0.3', 'bump', '1.0.3', '1.2.3'],
+        ['!= 1.0.3', '!= 1.0.3', 'bump', '1.0.0', '1.2.3'],
+        ['> 1.2.2', '> 1.0.3', 'bump', '1.0.4', '1.2.3'],
+        ['< 1.2.4', '< 1.0.3', 'bump', '1.0.0', '1.2.3'],
+        ['< 1.2.4', '< 1.2.2', 'bump', '1.0.0', '1.2.3'],
+        ['< 1.2.4', '< 1.2.3', 'bump', '1.0.0', '1.2.3'],
+        ['< 1.3', '< 1.2', 'bump', '1.0.0', '1.2.3'],
+        ['< 2', '< 1', 'bump', '0.9.0', '1.2.3'],
+        ['>= 1.2.3', '>= 1.0.3', 'bump', '1.0.3', '1.2.3'],
+        ['<= 1.2.3', '<= 1.0.3', 'bump', '1.0.3', '1.2.3'],
+        ['~> 1.2.0', '~> 1.0.3', 'bump', '1.0.3', '1.2.3'],
+        ['~> 1.0.0', '~> 1.0.3', 'bump', '1.0.3', '1.0.4'],
+        ['~> 4.7.0, >= 4.7.9', '~> 4.7, >= 4.7.4', 'bump', '4.7.5', '4.7.9'],
+      ].forEach(([expected, ...params]) => {
+        expect(semverRuby.getNewValue(...params)).toEqual(expected);
+      });
+    });
+
+    it('returns correct version for replace strategy', () => {
+      [
+        ['1.2.3', '1.0.3', 'replace', '1.0.3', '1.2.3'],
+        ['= 1.2.3', '= 1.0.3', 'replace', '1.0.3', '1.2.3'],
+        ['!= 1.0.3', '!= 1.0.3', 'replace', '1.0.0', '1.2.3'],
+        ['< 1.2.4', '< 1.0.3', 'replace', '1.0.0', '1.2.3'],
+        ['< 1.2.4', '< 1.2.2', 'replace', '1.0.0', '1.2.3'],
+        ['< 1.2.4', '< 1.2.3', 'replace', '1.0.0', '1.2.3'],
+        ['< 1.3', '< 1.2', 'replace', '1.0.0', '1.2.3'],
+        ['< 2', '< 1', 'replace', '0.9.0', '1.2.3'],
+        ['< 1.2.3', '< 1.2.3', 'replace', '1.0.0', '1.2.2'],
+        ['>= 1.0.3', '>= 1.0.3', 'replace', '1.0.3', '1.2.3'],
+        ['<= 1.2.3', '<= 1.0.3', 'replace', '1.0.0', '1.2.3'],
+        ['<= 1.0.3', '<= 1.0.3', 'replace', '1.0.0', '1.0.2'],
+        ['~> 1.2.0', '~> 1.0.3', 'replace', '1.0.0', '1.2.3'],
+        ['~> 1.0.3', '~> 1.0.3', 'replace', '1.0.0', '1.0.4'],
+        ['~> 4.7, >= 4.7.4', '~> 4.7, >= 4.7.4', 'replace', '1.0.0', '4.7.9'],
+      ].forEach(([expected, ...params]) => {
+        expect(semverRuby.getNewValue(...params)).toEqual(expected);
+      });
+    });
+  });
+});
diff --git a/yarn.lock b/yarn.lock
index 360ee70a94..929c081c10 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -173,6 +173,13 @@
   dependencies:
     symbol-observable "^1.2.0"
 
+"@snyk/ruby-semver@2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@snyk/ruby-semver/-/ruby-semver-2.0.0.tgz#2c07734a76cefde4e1574fb1456f963a989c8b95"
+  integrity sha512-ka2KbiUVI9doltXGGnwx/g+0AK3g5w2JF9gsz+Uparvfjs2/ywDVL0kpCOVFUuO31iPreiplKoWZ/hZsZ4BrFw==
+  dependencies:
+    lodash "^4"
+
 "@szmarczak/http-timer@^1.1.0":
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.1.tgz#6402258dfe467532b26649ef076b4d11f74fb612"
@@ -5095,7 +5102,7 @@ lodash.without@~4.4.0:
   resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
   integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
 
-lodash@4.17.11:
+lodash@4.17.11, lodash@^4:
   version "4.17.11"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
   integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
@@ -6083,7 +6090,6 @@ npm@6.5.0:
     cmd-shim "~2.0.2"
     columnify "~1.5.4"
     config-chain "^1.1.12"
-    debuglog "*"
     detect-indent "~5.0.0"
     detect-newline "^2.1.0"
     dezalgo "~1.0.3"
@@ -6098,7 +6104,6 @@ npm@6.5.0:
     has-unicode "~2.0.1"
     hosted-git-info "^2.7.1"
     iferr "^1.0.2"
-    imurmurhash "*"
     inflight "~1.0.6"
     inherits "~2.0.3"
     ini "^1.3.5"
@@ -6111,14 +6116,8 @@ npm@6.5.0:
     libnpx "^10.2.0"
     lock-verify "^2.0.2"
     lockfile "^1.0.4"
-    lodash._baseindexof "*"
     lodash._baseuniq "~4.6.0"
-    lodash._bindcallback "*"
-    lodash._cacheindexof "*"
-    lodash._createcache "*"
-    lodash._getnative "*"
     lodash.clonedeep "~4.5.0"
-    lodash.restparam "*"
     lodash.union "~4.6.0"
     lodash.uniq "~4.5.0"
     lodash.without "~4.4.0"
@@ -6157,7 +6156,6 @@ npm@6.5.0:
     read-package-json "^2.0.13"
     read-package-tree "^5.2.1"
     readable-stream "^2.3.6"
-    readdir-scoped-modules "*"
     request "^2.88.0"
     retry "^0.12.0"
     rimraf "~2.6.2"
-- 
GitLab