Skip to content

Commit 0ce87d6

Browse files
committed
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

Lines changed: 66 additions & 11 deletions
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

Lines changed: 27 additions & 4 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy