Skip to content

Commit 0ce87d6

Browse files
committedMar 23, 2021
Correctly handle prereleases/ANY ranges in subset
An "ANY" range (ie, `""`, `*`, etc.) does not include prerelease versions except when `includePrerelease` flag is set. Also, merely looking at the max/min boundaries of any ranges ignores the fact that the sub range maybe including prerelease versions that are excluded from the super range. For example, `>=1.2.3-pre.0` is _not_ a subset of `>=1.0.0`, because it inludes `1.2.3-pre.0`, `1.2.3-pre.1`, and so on. PR-URL: #377 Credit: @isaacs Close: #377 Reviewed-by: @wraithgar
1 parent 15ed208 commit 0ce87d6

File tree

2 files changed

+93
-15
lines changed

2 files changed

+93
-15
lines changed
 

‎ranges/subset.js

+66-11
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
const Range = require('../classes/range.js')
2-
const { ANY } = require('../classes/comparator.js')
2+
const Comparator = require('../classes/comparator.js')
3+
const { ANY } = Comparator
34
const satisfies = require('../functions/satisfies.js')
45
const compare = require('../functions/compare.js')
56

67
// Complex range `r1 || r2 || ...` is a subset of `R1 || R2 || ...` iff:
7-
// - Every simple range `r1, r2, ...` is a subset of some `R1, R2, ...`
8+
// - Every simple range `r1, r2, ...` is a null set, OR
9+
// - Every simple range `r1, r2, ...` which is not a null set is a subset of
10+
// some `R1, R2, ...`
811
//
912
// Simple range `c1 c2 ...` is a subset of simple range `C1 C2 ...` iff:
10-
// - If C is only the ANY comparator
11-
// - return true
1213
// - If c is only the ANY comparator
1314
// - If C is only the ANY comparator, return true
14-
// - Else return false
15+
// - Else if in prerelease mode, return false
16+
// - else replace c with `[>=0.0.0]`
17+
// - If C is only the ANY comparator
18+
// - if in prerelease mode, return true
19+
// - else replace C with `[>=0.0.0]`
1520
// - Let EQ be the set of = comparators in c
1621
// - If EQ is more than one, return true (null set)
1722
// - Let GT be the highest > or >= comparator in c
1823
// - Let LT be the lowest < or <= comparator in c
1924
// - If GT and LT, and GT.semver > LT.semver, return true (null set)
25+
// - If any C is a = range, and GT or LT are set, return false
2026
// - If EQ
2127
// - If GT, and EQ does not satisfy GT, return true (null set)
2228
// - If LT, and EQ does not satisfy LT, return true (null set)
@@ -25,13 +31,16 @@ const compare = require('../functions/compare.js')
2531
// - If GT
2632
// - If GT.semver is lower than any > or >= comp in C, return false
2733
// - If GT is >=, and GT.semver does not satisfy every C, return false
34+
// - If GT.semver has a prerelease, and not in prerelease mode
35+
// - If no C has a prerelease and the GT.semver tuple, return false
2836
// - If LT
2937
// - If LT.semver is greater than any < or <= comp in C, return false
3038
// - If LT is <=, and LT.semver does not satisfy every C, return false
31-
// - If any C is a = range, and GT or LT are set, return false
39+
// - If GT.semver has a prerelease, and not in prerelease mode
40+
// - If no C has a prerelease and the LT.semver tuple, return false
3241
// - Else return true
3342

34-
const subset = (sub, dom, options) => {
43+
const subset = (sub, dom, options = {}) => {
3544
if (sub === dom)
3645
return true
3746

@@ -60,11 +69,21 @@ const simpleSubset = (sub, dom, options) => {
6069
if (sub === dom)
6170
return true
6271

63-
if (dom.length === 1 && dom[0].semver === ANY)
64-
return true
72+
if (sub.length === 1 && sub[0].semver === ANY) {
73+
if (dom.length === 1 && dom[0].semver === ANY)
74+
return true
75+
else if (options.includePrerelease)
76+
sub = [ new Comparator('>=0.0.0-0') ]
77+
else
78+
sub = [ new Comparator('>=0.0.0') ]
79+
}
6580

66-
if (sub.length === 1 && sub[0].semver === ANY)
67-
return dom.length === 1 && dom[0].semver === ANY
81+
if (dom.length === 1 && dom[0].semver === ANY) {
82+
if (options.includePrerelease)
83+
return true
84+
else
85+
dom = [ new Comparator('>=0.0.0') ]
86+
}
6887

6988
const eqSet = new Set()
7089
let gt, lt
@@ -107,10 +126,32 @@ const simpleSubset = (sub, dom, options) => {
107126

108127
let higher, lower
109128
let hasDomLT, hasDomGT
129+
// if the subset has a prerelease, we need a comparator in the superset
130+
// with the same tuple and a prerelease, or it's not a subset
131+
let needDomLTPre = lt &&
132+
!options.includePrerelease &&
133+
lt.semver.prerelease.length ? lt.semver : false
134+
let needDomGTPre = gt &&
135+
!options.includePrerelease &&
136+
gt.semver.prerelease.length ? gt.semver : false
137+
// exception: <1.2.3-0 is the same as <1.2.3
138+
if (needDomLTPre && needDomLTPre.prerelease.length === 1 &&
139+
lt.operator === '<' && needDomLTPre.prerelease[0] === 0) {
140+
needDomLTPre = false
141+
}
142+
110143
for (const c of dom) {
111144
hasDomGT = hasDomGT || c.operator === '>' || c.operator === '>='
112145
hasDomLT = hasDomLT || c.operator === '<' || c.operator === '<='
113146
if (gt) {
147+
if (needDomGTPre) {
148+
if (c.semver.prerelease && c.semver.prerelease.length &&
149+
c.semver.major === needDomGTPre.major &&
150+
c.semver.minor === needDomGTPre.minor &&
151+
c.semver.patch === needDomGTPre.patch) {
152+
needDomGTPre = false
153+
}
154+
}
114155
if (c.operator === '>' || c.operator === '>=') {
115156
higher = higherGT(gt, c, options)
116157
if (higher === c && higher !== gt)
@@ -119,6 +160,14 @@ const simpleSubset = (sub, dom, options) => {
119160
return false
120161
}
121162
if (lt) {
163+
if (needDomLTPre) {
164+
if (c.semver.prerelease && c.semver.prerelease.length &&
165+
c.semver.major === needDomLTPre.major &&
166+
c.semver.minor === needDomLTPre.minor &&
167+
c.semver.patch === needDomLTPre.patch) {
168+
needDomLTPre = false
169+
}
170+
}
122171
if (c.operator === '<' || c.operator === '<=') {
123172
lower = lowerLT(lt, c, options)
124173
if (lower === c && lower !== lt)
@@ -139,6 +188,12 @@ const simpleSubset = (sub, dom, options) => {
139188
if (lt && hasDomGT && !gt && gtltComp !== 0)
140189
return false
141190

191+
// we needed a prerelease range in a specific tuple, but didn't get one
192+
// then this isn't a subset. eg >=1.2.3-pre is not a subset of >=1.0.0,
193+
// because it includes prereleases in the 1.2.3 tuple
194+
if (needDomGTPre || needDomLTPre)
195+
return false
196+
142197
return true
143198
}
144199

‎test/ranges/subset.js

+27-4
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,26 @@ const cases = [
1818
// everything is a subset of *
1919
['1.2.3', '*', true],
2020
['^1.2.3', '*', true],
21-
['^1.2.3-pre.0', '*', true],
21+
['^1.2.3-pre.0', '*', false],
22+
['^1.2.3-pre.0', '*', true, { includePrerelease: true }],
2223
['1 || 2 || 3', '*', true],
2324

25+
// prerelease edge cases
26+
['^1.2.3-pre.0', '>=1.0.0', false],
27+
['^1.2.3-pre.0', '>=1.0.0', true, { includePrerelease: true }],
28+
['^1.2.3-pre.0', '>=1.2.3-pre.0', true],
29+
['^1.2.3-pre.0', '>=1.2.3-pre.0', true, { includePrerelease: true }],
30+
['>1.2.3-pre.0', '>=1.2.3-pre.0', true],
31+
['>1.2.3-pre.0', '>1.2.3-pre.0 || 2', true],
32+
['1 >1.2.3-pre.0', '>1.2.3-pre.0', true],
33+
['1 <=1.2.3-pre.0', '>=1.0.0-0', false],
34+
['1 <=1.2.3-pre.0', '>=1.0.0-0', true, { includePrerelease: true }],
35+
['1 <=1.2.3-pre.0', '<=1.2.3-pre.0', true],
36+
['1 <=1.2.3-pre.0', '<=1.2.3-pre.0', true, { includePrerelease: true }],
37+
['<1.2.3-pre.0', '<=1.2.3-pre.0', true],
38+
['<1.2.3-pre.0', '<1.2.3-pre.0 || 2', true],
39+
['1 <1.2.3-pre.0', '<1.2.3-pre.0', true],
40+
2441
['*', '*', true],
2542
['', '*', true],
2643
['*', '', true],
@@ -29,9 +46,16 @@ const cases = [
2946
// >=0.0.0 is like * in non-prerelease mode
3047
// >=0.0.0-0 is like * in prerelease mode
3148
['*', '>=0.0.0-0', true, { includePrerelease: true }],
49+
50+
// true because these are identical in non-PR mode
3251
['*', '>=0.0.0', true],
52+
53+
// false because * includes 0.0.0-0 in PR mode
3354
['*', '>=0.0.0', false, { includePrerelease: true }],
34-
['*', '>=0.0.0-0', false],
55+
56+
// true because * doesn't include 0.0.0-0 in non-PR mode
57+
['*', '>=0.0.0-0', true],
58+
3559
['^2 || ^3 || ^4', '>=1', true],
3660
['^2 || ^3 || ^4', '>1', true],
3761
['^2 || ^3 || ^4', '>=2', true],
@@ -79,9 +103,8 @@ const cases = [
79103
['>2.0.0', '>=2.0.0', true],
80104
]
81105

82-
83106
t.plan(cases.length + 1)
84-
cases.forEach(([sub, dom, expect, options = {}]) => {
107+
cases.forEach(([sub, dom, expect, options]) => {
85108
const msg = `${sub || "''"}${dom || "''"} = ${expect}` +
86109
(options ? ' ' + Object.keys(options).join(',') : '')
87110
t.equal(subset(sub, dom, options), expect, msg)

0 commit comments

Comments
 (0)
Please sign in to comment.