Skip to content

Commit 2b5ad50

Browse files
committed
Add method for simplifying ranges
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
1 parent 6e7982f commit 2b5ad50

File tree

4 files changed

+96
-0
lines changed

4 files changed

+96
-0
lines changed

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const semverOutside = require('semver/ranges/outside')
7878
const semverGtr = require('semver/ranges/gtr')
7979
const semverLtr = require('semver/ranges/ltr')
8080
const semverIntersects = require('semver/ranges/intersects')
81+
const simplifyRange = require('semver/ranges/simplify')
8182
```
8283
8384
As a command-line utility:
@@ -446,6 +447,14 @@ strings that they parse.
446447
`hilo` argument must be either the string `'>'` or `'<'`. (This is
447448
the function called by `gtr` and `ltr`.)
448449
* `intersects(range)`: Return true if any of the ranges comparators intersect
450+
* `simplifyRange(versions, range)`: Return a "simplified" range that
451+
matches the same items in `versions` list as the range specified. Note
452+
that it does *not* guarantee that it would match the same versions in all
453+
cases, only for the set of versions provided. This is useful when
454+
generating ranges by joining together multiple versions with `||`
455+
programmatically, to provide the user with something a bit more
456+
ergonomic. If the provided range is shorter in string-length than the
457+
generated range, then that is returned.
449458

450459
Note that, since ranges may be non-contiguous, a version might not be
451460
greater than a range, less than a range, *or* satisfy a range! For

index.js

+1
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,5 @@ module.exports = {
4343
gtr: require('./ranges/gtr'),
4444
ltr: require('./ranges/ltr'),
4545
intersects: require('./ranges/intersects'),
46+
simplifyRange: require('./ranges/simplify'),
4647
}

ranges/simplify.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// given a set of versions and a range, create a "simplified" range
2+
// that includes the same versions that the original range does
3+
// If the original range is shorter than the simplified one, return that.
4+
const satisfies = require('../functions/satisfies.js')
5+
const compare = require('../functions/compare.js')
6+
module.exports = (versions, range, options) => {
7+
const set = []
8+
let min = null
9+
let prev = null
10+
const v = versions.sort((a, b) => compare(a, b, options))
11+
for (const version of v) {
12+
const included = satisfies(version, range, options)
13+
if (included) {
14+
prev = version
15+
if (!min)
16+
min = version
17+
} else {
18+
if (prev) {
19+
set.push([min, prev])
20+
}
21+
prev = null
22+
min = null
23+
}
24+
}
25+
if (min)
26+
set.push([min, null])
27+
28+
const ranges = []
29+
for (const [min, max] of set) {
30+
if (min === max)
31+
ranges.push(min)
32+
else if (!max && min === v[0])
33+
ranges.push('*')
34+
else if (!max)
35+
ranges.push(`>=${min}`)
36+
else if (min === v[0])
37+
ranges.push(`<=${max}`)
38+
else
39+
ranges.push(`${min} - ${max}`)
40+
}
41+
const simplified = ranges.join(' || ')
42+
const original = typeof range.raw === 'string' ? range.raw : String(range)
43+
return simplified.length < original.length ? simplified : range
44+
}

test/ranges/simplify.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const simplify = require('../../ranges/simplify.js')
2+
const Range = require('../../classes/range.js')
3+
const t = require('tap')
4+
const versions = [
5+
'1.0.0',
6+
'1.0.1',
7+
'1.0.2',
8+
'1.0.3',
9+
'1.0.4',
10+
'1.1.0',
11+
'1.1.1',
12+
'1.1.2',
13+
'1.2.0',
14+
'1.2.1',
15+
'1.2.2',
16+
'1.2.3',
17+
'1.2.4',
18+
'1.2.5',
19+
'2.0.0',
20+
'2.0.1',
21+
'2.1.0',
22+
'2.1.1',
23+
'2.1.2',
24+
'2.2.0',
25+
'2.2.1',
26+
'2.2.2',
27+
'2.3.0',
28+
'2.3.1',
29+
'2.4.0',
30+
'3.0.0',
31+
'3.1.0',
32+
'3.2.0',
33+
'3.3.0',
34+
]
35+
36+
t.equal(simplify(versions, '1.x'), '1.x')
37+
t.equal(simplify(versions, '1.0.0 || 1.0.1 || 1.0.2 || 1.0.3 || 1.0.4'), '<=1.0.4')
38+
t.equal(simplify(versions, new Range('1.0.0 || 1.0.1 || 1.0.2 || 1.0.3 || 1.0.4')), '<=1.0.4')
39+
t.equal(simplify(versions, '>=3.0.0 <3.1.0'), '3.0.0')
40+
t.equal(simplify(versions, '3.0.0 || 3.1 || 3.2 || 3.3'), '>=3.0.0')
41+
t.equal(simplify(versions, '1 || 2 || 3'), '*')
42+
t.equal(simplify(versions, '2.1 || 2.2 || 2.3'), '2.1.0 - 2.3.1')

0 commit comments

Comments
 (0)