Skip to content

Commit bcab95a

Browse files
committedMay 8, 2020
Reduce unnecessary Comparators in Range set
There are a few cases that lead to Range's Comparator sets being longer than strictly necessary. This reduces performance of methods that iterate over ranges repeatedly (for example, `intersects` and `subset`), and leads to some confusing toString output like turning `x || * || X` into `||||` instead of `*`. - If any simple range in the set contains the null set <0.0.0-0, then the entire simple range is the null set. `2.x <0.0.0-0` is the same as just `<0.0.0-0`. (This is used for `>*` and `<*`, which cannot match anything.) - Ensure that a given Comparator will only occur once within each simple range set. `2.3.x ^2.3` doesn't need to include `>=2.3.0` more than once. - If a simple range set contains more than one comparator, remove any `*` comparators. `* >=2.3.4` is the same as just `>=2.3.4`. This was already being done in the cast to a string, but some `ANY` Comparators would be left behind in the set used for matching. - If a Range set contains the simple range `*`, then drop any other simple ranges in the set. `* || 2.x` is the same as `*`. There's still some unnecessary comparators in there. For example, the range `2.3 ^2.3.4` parses to `>=2.3.0 <2.4.0-0 >=2.3.4 <3.0.0-0`. Of course, anything that is `<2.4.0-0` is also `<3.0.0-0`, and anything that is `>=2.3.4` is also `>=2.3.0`, so the `<3.0.0-0` and `>=2.3.0` Comparators are not necessary. But simplifying those out would be a bit more work. To do that, we could walk the set of Comparators checking to see if they are a subset of any other Comparators in the list, and if so, removing them. The subset check would not have to be a full Range.subset(); we could just see if the gtlt points in the same direction, and if one semver is greater than the other. It's an O(n^2) operation, but one on typically very small n. PR-URL: #324 Credit: @isaacs Close: #324
1 parent 226e6dc commit bcab95a

File tree

3 files changed

+48
-5
lines changed

3 files changed

+48
-5
lines changed
 

‎classes/range.js

+38-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@ class Range {
4646
throw new TypeError(`Invalid SemVer Range: ${range}`)
4747
}
4848

49+
// if we have any that are not the null set, throw out null sets.
50+
if (this.set.length > 1) {
51+
// keep the first one, in case they're all null sets
52+
const first = this.set[0]
53+
this.set = this.set.filter(c => !isNullSet(c[0]))
54+
if (this.set.length === 0)
55+
this.set = [first]
56+
else if (this.set.length > 1) {
57+
// if we have any that are *, then the range is just *
58+
for (const c of this.set) {
59+
if (c.length === 1 && isAny(c[0])) {
60+
this.set = [c]
61+
break
62+
}
63+
}
64+
}
65+
}
66+
4967
this.format()
5068
}
5169

@@ -87,15 +105,31 @@ class Range {
87105
// ready to be split into comparators.
88106

89107
const compRe = loose ? re[t.COMPARATORLOOSE] : re[t.COMPARATOR]
90-
return range
108+
const rangeList = range
91109
.split(' ')
92110
.map(comp => parseComparator(comp, this.options))
93111
.join(' ')
94112
.split(/\s+/)
113+
// >=0.0.0 is equivalent to *
95114
.map(comp => replaceGTE0(comp, this.options))
96115
// in loose mode, throw out any that are not valid comparators
97116
.filter(this.options.loose ? comp => !!comp.match(compRe) : () => true)
98117
.map(comp => new Comparator(comp, this.options))
118+
119+
// if any comparators are the null set, then replace with JUST null set
120+
// if more than one comparator, remove any * comparators
121+
// also, don't include the same comparator more than once
122+
const l = rangeList.length
123+
const rangeMap = new Map()
124+
for (const comp of rangeList) {
125+
if (isNullSet(comp))
126+
return [comp]
127+
rangeMap.set(comp.value, comp)
128+
}
129+
if (rangeMap.size > 1 && rangeMap.has(''))
130+
rangeMap.delete('')
131+
132+
return [...rangeMap.values()]
99133
}
100134

101135
intersects (range, options) {
@@ -155,6 +189,9 @@ const {
155189
caretTrimReplace
156190
} = require('../internal/re')
157191

192+
const isNullSet = c => c.value === '<0.0.0-0'
193+
const isAny = c => c.value === ''
194+
158195
// take a set of comparators and determine whether there
159196
// exists a version which can satisfy it
160197
const isSatisfiable = (comparators, options) => {

‎test/fixtures/range-parse.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ module.exports = [
3838
['>=0.2.3 || <0.0.1', '>=0.2.3||<0.0.1'],
3939
['>=0.2.3 || <0.0.1', '>=0.2.3||<0.0.1'],
4040
['>=0.2.3 || <0.0.1', '>=0.2.3||<0.0.1'],
41-
['||', '||'],
41+
['||', '*'],
4242
['2.x.x', '>=2.0.0 <3.0.0-0'],
4343
['1.2.x', '>=1.2.0 <1.3.0-0'],
4444
['1.2.x || 2.x', '>=1.2.0 <1.3.0-0||>=2.0.0 <3.0.0-0'],
@@ -79,12 +79,14 @@ module.exports = [
7979
['>01.02.03', null],
8080
['~1.2.3beta', '>=1.2.3-beta <1.3.0-0', { loose: true }],
8181
['~1.2.3beta', null],
82-
['^ 1.2 ^ 1', '>=1.2.0 <2.0.0-0 >=1.0.0 <2.0.0-0'],
82+
['^ 1.2 ^ 1', '>=1.2.0 <2.0.0-0 >=1.0.0'],
8383
['1.2 - 3.4.5', '>=1.2.0 <=3.4.5'],
8484
['1.2.3 - 3.4', '>=1.2.3 <3.5.0-0'],
8585
['1.2 - 3.4', '>=1.2.0 <3.5.0-0'],
8686
['>1', '>=2.0.0'],
8787
['>1.2', '>=1.3.0'],
8888
['>X', '<0.0.0-0'],
8989
['<X', '<0.0.0-0'],
90+
['<x <* || >* 2.x', '<0.0.0-0'],
91+
['>x 2.x || * || <x', '*'],
9092
]

‎test/ranges/to-comparators.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ test('comparators test', (t) => {
3737
['>=0.2.3 || <0.0.1', [['>=0.2.3'], ['<0.0.1']]],
3838
['>=0.2.3 || <0.0.1', [['>=0.2.3'], ['<0.0.1']]],
3939
['>=0.2.3 || <0.0.1', [['>=0.2.3'], ['<0.0.1']]],
40-
['||', [[''], ['']]],
40+
['||', [['']]],
4141
['2.x.x', [['>=2.0.0', '<3.0.0-0']]],
4242
['1.2.x', [['>=1.2.0', '<1.3.0-0']]],
4343
['1.2.x || 2.x', [['>=1.2.0', '<1.3.0-0'], ['>=2.0.0', '<3.0.0-0']]],
@@ -72,7 +72,11 @@ test('comparators test', (t) => {
7272
['1.2.3 - 3.4', [['>=1.2.3', '<3.5.0-0']]],
7373
['1.2.3 - 3', [['>=1.2.3', '<4.0.0-0']]],
7474
['>*', [['<0.0.0-0']]],
75-
['<*', [['<0.0.0-0']]]
75+
['<*', [['<0.0.0-0']]],
76+
['>X', [['<0.0.0-0']]],
77+
['<X', [['<0.0.0-0']]],
78+
['<x <* || >* 2.x', [['<0.0.0-0']]],
79+
['>x 2.x || * || <x', [['']]],
7680
].forEach(([pre, wanted]) => {
7781
const found = toComparators(pre)
7882
const jw = JSON.stringify(wanted)

0 commit comments

Comments
 (0)
Please sign in to comment.