Skip to content

Commit

Permalink
Add method for simplifying ranges
Browse files Browse the repository at this point in the history
Need this for a case where we programmatically generate a list of all
the versions subject to a security vulnerability by virtue of depending
exclusively on vulnerable versions of a given dependency.

While it's not a perfect heuristic, with this method on semver, instead
of printing out something long and confusing like this:

    8.0.1 || 8.0.2 || 9.0.0 || 9.0.1 || 10.0.0-alpha.0 || 10.0.0-alpha.1
    || 10.0.0-alpha.2 || 10.0.0-alpha.3 || 10.0.0-alpha.4 || 10.0.0 ||
    10.0.1 || 10.0.2 || 10.0.3 || 10.1.0 || 10.1.1 || 10.1.2 || 11.0.0
    || 11.1.0 || 12.0.0-candidate.0 || 12.0.0 || 12.0.1

we can show this:

    8.0.1 - 11.1.0 || 12.0.0-candidate.0 - 12.0.1
  • Loading branch information
isaacs committed Apr 6, 2020
1 parent 6e7982f commit 2b5ad50
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 0 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const semverOutside = require('semver/ranges/outside')
const semverGtr = require('semver/ranges/gtr')
const semverLtr = require('semver/ranges/ltr')
const semverIntersects = require('semver/ranges/intersects')
const simplifyRange = require('semver/ranges/simplify')
```
As a command-line utility:
Expand Down Expand Up @@ -446,6 +447,14 @@ strings that they parse.
`hilo` argument must be either the string `'>'` or `'<'`. (This is
the function called by `gtr` and `ltr`.)
* `intersects(range)`: Return true if any of the ranges comparators intersect
* `simplifyRange(versions, range)`: Return a "simplified" range that
matches the same items in `versions` list as the range specified. Note
that it does *not* guarantee that it would match the same versions in all
cases, only for the set of versions provided. This is useful when
generating ranges by joining together multiple versions with `||`
programmatically, to provide the user with something a bit more
ergonomic. If the provided range is shorter in string-length than the
generated range, then that is returned.

Note that, since ranges may be non-contiguous, a version might not be
greater than a range, less than a range, *or* satisfy a range! For
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ module.exports = {
gtr: require('./ranges/gtr'),
ltr: require('./ranges/ltr'),
intersects: require('./ranges/intersects'),
simplifyRange: require('./ranges/simplify'),
}
44 changes: 44 additions & 0 deletions ranges/simplify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// given a set of versions and a range, create a "simplified" range
// that includes the same versions that the original range does
// If the original range is shorter than the simplified one, return that.
const satisfies = require('../functions/satisfies.js')
const compare = require('../functions/compare.js')
module.exports = (versions, range, options) => {
const set = []
let min = null
let prev = null
const v = versions.sort((a, b) => compare(a, b, options))
for (const version of v) {
const included = satisfies(version, range, options)
if (included) {
prev = version
if (!min)
min = version
} else {
if (prev) {
set.push([min, prev])
}
prev = null
min = null
}
}
if (min)
set.push([min, null])

const ranges = []
for (const [min, max] of set) {
if (min === max)
ranges.push(min)
else if (!max && min === v[0])
ranges.push('*')
else if (!max)
ranges.push(`>=${min}`)
else if (min === v[0])
ranges.push(`<=${max}`)
else
ranges.push(`${min} - ${max}`)
}
const simplified = ranges.join(' || ')
const original = typeof range.raw === 'string' ? range.raw : String(range)
return simplified.length < original.length ? simplified : range
}
42 changes: 42 additions & 0 deletions test/ranges/simplify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const simplify = require('../../ranges/simplify.js')
const Range = require('../../classes/range.js')
const t = require('tap')
const versions = [
'1.0.0',
'1.0.1',
'1.0.2',
'1.0.3',
'1.0.4',
'1.1.0',
'1.1.1',
'1.1.2',
'1.2.0',
'1.2.1',
'1.2.2',
'1.2.3',
'1.2.4',
'1.2.5',
'2.0.0',
'2.0.1',
'2.1.0',
'2.1.1',
'2.1.2',
'2.2.0',
'2.2.1',
'2.2.2',
'2.3.0',
'2.3.1',
'2.4.0',
'3.0.0',
'3.1.0',
'3.2.0',
'3.3.0',
]

t.equal(simplify(versions, '1.x'), '1.x')
t.equal(simplify(versions, '1.0.0 || 1.0.1 || 1.0.2 || 1.0.3 || 1.0.4'), '<=1.0.4')
t.equal(simplify(versions, new Range('1.0.0 || 1.0.1 || 1.0.2 || 1.0.3 || 1.0.4')), '<=1.0.4')
t.equal(simplify(versions, '>=3.0.0 <3.1.0'), '3.0.0')
t.equal(simplify(versions, '3.0.0 || 3.1 || 3.2 || 3.3'), '>=3.0.0')
t.equal(simplify(versions, '1 || 2 || 3'), '*')
t.equal(simplify(versions, '2.1 || 2.2 || 2.3'), '2.1.0 - 2.3.1')

0 comments on commit 2b5ad50

Please sign in to comment.