• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// hoisted class for cyclic dependency
2class Range {
3  constructor (range, options) {
4    options = parseOptions(options)
5
6    if (range instanceof Range) {
7      if (
8        range.loose === !!options.loose &&
9        range.includePrerelease === !!options.includePrerelease
10      ) {
11        return range
12      } else {
13        return new Range(range.raw, options)
14      }
15    }
16
17    if (range instanceof Comparator) {
18      // just put it in the set and return
19      this.raw = range.value
20      this.set = [[range]]
21      this.format()
22      return this
23    }
24
25    this.options = options
26    this.loose = !!options.loose
27    this.includePrerelease = !!options.includePrerelease
28
29    // First reduce all whitespace as much as possible so we do not have to rely
30    // on potentially slow regexes like \s*. This is then stored and used for
31    // future error messages as well.
32    this.raw = range
33      .trim()
34      .split(/\s+/)
35      .join(' ')
36
37    // First, split on ||
38    this.set = this.raw
39      .split('||')
40      // map the range to a 2d array of comparators
41      .map(r => this.parseRange(r.trim()))
42      // throw out any comparator lists that are empty
43      // this generally means that it was not a valid range, which is allowed
44      // in loose mode, but will still throw if the WHOLE range is invalid.
45      .filter(c => c.length)
46
47    if (!this.set.length) {
48      throw new TypeError(`Invalid SemVer Range: ${this.raw}`)
49    }
50
51    // if we have any that are not the null set, throw out null sets.
52    if (this.set.length > 1) {
53      // keep the first one, in case they're all null sets
54      const first = this.set[0]
55      this.set = this.set.filter(c => !isNullSet(c[0]))
56      if (this.set.length === 0) {
57        this.set = [first]
58      } else if (this.set.length > 1) {
59        // if we have any that are *, then the range is just *
60        for (const c of this.set) {
61          if (c.length === 1 && isAny(c[0])) {
62            this.set = [c]
63            break
64          }
65        }
66      }
67    }
68
69    this.format()
70  }
71
72  format () {
73    this.range = this.set
74      .map((comps) => comps.join(' ').trim())
75      .join('||')
76      .trim()
77    return this.range
78  }
79
80  toString () {
81    return this.range
82  }
83
84  parseRange (range) {
85    // memoize range parsing for performance.
86    // this is a very hot path, and fully deterministic.
87    const memoOpts =
88      (this.options.includePrerelease && FLAG_INCLUDE_PRERELEASE) |
89      (this.options.loose && FLAG_LOOSE)
90    const memoKey = memoOpts + ':' + range
91    const cached = cache.get(memoKey)
92    if (cached) {
93      return cached
94    }
95
96    const loose = this.options.loose
97    // `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4`
98    const hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE]
99    range = range.replace(hr, hyphenReplace(this.options.includePrerelease))
100    debug('hyphen replace', range)
101
102    // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5`
103    range = range.replace(re[t.COMPARATORTRIM], comparatorTrimReplace)
104    debug('comparator trim', range)
105
106    // `~ 1.2.3` => `~1.2.3`
107    range = range.replace(re[t.TILDETRIM], tildeTrimReplace)
108    debug('tilde trim', range)
109
110    // `^ 1.2.3` => `^1.2.3`
111    range = range.replace(re[t.CARETTRIM], caretTrimReplace)
112    debug('caret trim', range)
113
114    // At this point, the range is completely trimmed and
115    // ready to be split into comparators.
116
117    let rangeList = range
118      .split(' ')
119      .map(comp => parseComparator(comp, this.options))
120      .join(' ')
121      .split(/\s+/)
122      // >=0.0.0 is equivalent to *
123      .map(comp => replaceGTE0(comp, this.options))
124
125    if (loose) {
126      // in loose mode, throw out any that are not valid comparators
127      rangeList = rangeList.filter(comp => {
128        debug('loose invalid filter', comp, this.options)
129        return !!comp.match(re[t.COMPARATORLOOSE])
130      })
131    }
132    debug('range list', rangeList)
133
134    // if any comparators are the null set, then replace with JUST null set
135    // if more than one comparator, remove any * comparators
136    // also, don't include the same comparator more than once
137    const rangeMap = new Map()
138    const comparators = rangeList.map(comp => new Comparator(comp, this.options))
139    for (const comp of comparators) {
140      if (isNullSet(comp)) {
141        return [comp]
142      }
143      rangeMap.set(comp.value, comp)
144    }
145    if (rangeMap.size > 1 && rangeMap.has('')) {
146      rangeMap.delete('')
147    }
148
149    const result = [...rangeMap.values()]
150    cache.set(memoKey, result)
151    return result
152  }
153
154  intersects (range, options) {
155    if (!(range instanceof Range)) {
156      throw new TypeError('a Range is required')
157    }
158
159    return this.set.some((thisComparators) => {
160      return (
161        isSatisfiable(thisComparators, options) &&
162        range.set.some((rangeComparators) => {
163          return (
164            isSatisfiable(rangeComparators, options) &&
165            thisComparators.every((thisComparator) => {
166              return rangeComparators.every((rangeComparator) => {
167                return thisComparator.intersects(rangeComparator, options)
168              })
169            })
170          )
171        })
172      )
173    })
174  }
175
176  // if ANY of the sets match ALL of its comparators, then pass
177  test (version) {
178    if (!version) {
179      return false
180    }
181
182    if (typeof version === 'string') {
183      try {
184        version = new SemVer(version, this.options)
185      } catch (er) {
186        return false
187      }
188    }
189
190    for (let i = 0; i < this.set.length; i++) {
191      if (testSet(this.set[i], version, this.options)) {
192        return true
193      }
194    }
195    return false
196  }
197}
198
199module.exports = Range
200
201const LRU = require('lru-cache')
202const cache = new LRU({ max: 1000 })
203
204const parseOptions = require('../internal/parse-options')
205const Comparator = require('./comparator')
206const debug = require('../internal/debug')
207const SemVer = require('./semver')
208const {
209  safeRe: re,
210  t,
211  comparatorTrimReplace,
212  tildeTrimReplace,
213  caretTrimReplace,
214} = require('../internal/re')
215const { FLAG_INCLUDE_PRERELEASE, FLAG_LOOSE } = require('../internal/constants')
216
217const isNullSet = c => c.value === '<0.0.0-0'
218const isAny = c => c.value === ''
219
220// take a set of comparators and determine whether there
221// exists a version which can satisfy it
222const isSatisfiable = (comparators, options) => {
223  let result = true
224  const remainingComparators = comparators.slice()
225  let testComparator = remainingComparators.pop()
226
227  while (result && remainingComparators.length) {
228    result = remainingComparators.every((otherComparator) => {
229      return testComparator.intersects(otherComparator, options)
230    })
231
232    testComparator = remainingComparators.pop()
233  }
234
235  return result
236}
237
238// comprised of xranges, tildes, stars, and gtlt's at this point.
239// already replaced the hyphen ranges
240// turn into a set of JUST comparators.
241const parseComparator = (comp, options) => {
242  debug('comp', comp, options)
243  comp = replaceCarets(comp, options)
244  debug('caret', comp)
245  comp = replaceTildes(comp, options)
246  debug('tildes', comp)
247  comp = replaceXRanges(comp, options)
248  debug('xrange', comp)
249  comp = replaceStars(comp, options)
250  debug('stars', comp)
251  return comp
252}
253
254const isX = id => !id || id.toLowerCase() === 'x' || id === '*'
255
256// ~, ~> --> * (any, kinda silly)
257// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0-0
258// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0-0
259// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0-0
260// ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0-0
261// ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0-0
262// ~0.0.1 --> >=0.0.1 <0.1.0-0
263const replaceTildes = (comp, options) => {
264  return comp
265    .trim()
266    .split(/\s+/)
267    .map((c) => replaceTilde(c, options))
268    .join(' ')
269}
270
271const replaceTilde = (comp, options) => {
272  const r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE]
273  return comp.replace(r, (_, M, m, p, pr) => {
274    debug('tilde', comp, _, M, m, p, pr)
275    let ret
276
277    if (isX(M)) {
278      ret = ''
279    } else if (isX(m)) {
280      ret = `>=${M}.0.0 <${+M + 1}.0.0-0`
281    } else if (isX(p)) {
282      // ~1.2 == >=1.2.0 <1.3.0-0
283      ret = `>=${M}.${m}.0 <${M}.${+m + 1}.0-0`
284    } else if (pr) {
285      debug('replaceTilde pr', pr)
286      ret = `>=${M}.${m}.${p}-${pr
287      } <${M}.${+m + 1}.0-0`
288    } else {
289      // ~1.2.3 == >=1.2.3 <1.3.0-0
290      ret = `>=${M}.${m}.${p
291      } <${M}.${+m + 1}.0-0`
292    }
293
294    debug('tilde return', ret)
295    return ret
296  })
297}
298
299// ^ --> * (any, kinda silly)
300// ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0-0
301// ^2.0, ^2.0.x --> >=2.0.0 <3.0.0-0
302// ^1.2, ^1.2.x --> >=1.2.0 <2.0.0-0
303// ^1.2.3 --> >=1.2.3 <2.0.0-0
304// ^1.2.0 --> >=1.2.0 <2.0.0-0
305// ^0.0.1 --> >=0.0.1 <0.0.2-0
306// ^0.1.0 --> >=0.1.0 <0.2.0-0
307const replaceCarets = (comp, options) => {
308  return comp
309    .trim()
310    .split(/\s+/)
311    .map((c) => replaceCaret(c, options))
312    .join(' ')
313}
314
315const replaceCaret = (comp, options) => {
316  debug('caret', comp, options)
317  const r = options.loose ? re[t.CARETLOOSE] : re[t.CARET]
318  const z = options.includePrerelease ? '-0' : ''
319  return comp.replace(r, (_, M, m, p, pr) => {
320    debug('caret', comp, _, M, m, p, pr)
321    let ret
322
323    if (isX(M)) {
324      ret = ''
325    } else if (isX(m)) {
326      ret = `>=${M}.0.0${z} <${+M + 1}.0.0-0`
327    } else if (isX(p)) {
328      if (M === '0') {
329        ret = `>=${M}.${m}.0${z} <${M}.${+m + 1}.0-0`
330      } else {
331        ret = `>=${M}.${m}.0${z} <${+M + 1}.0.0-0`
332      }
333    } else if (pr) {
334      debug('replaceCaret pr', pr)
335      if (M === '0') {
336        if (m === '0') {
337          ret = `>=${M}.${m}.${p}-${pr
338          } <${M}.${m}.${+p + 1}-0`
339        } else {
340          ret = `>=${M}.${m}.${p}-${pr
341          } <${M}.${+m + 1}.0-0`
342        }
343      } else {
344        ret = `>=${M}.${m}.${p}-${pr
345        } <${+M + 1}.0.0-0`
346      }
347    } else {
348      debug('no pr')
349      if (M === '0') {
350        if (m === '0') {
351          ret = `>=${M}.${m}.${p
352          }${z} <${M}.${m}.${+p + 1}-0`
353        } else {
354          ret = `>=${M}.${m}.${p
355          }${z} <${M}.${+m + 1}.0-0`
356        }
357      } else {
358        ret = `>=${M}.${m}.${p
359        } <${+M + 1}.0.0-0`
360      }
361    }
362
363    debug('caret return', ret)
364    return ret
365  })
366}
367
368const replaceXRanges = (comp, options) => {
369  debug('replaceXRanges', comp, options)
370  return comp
371    .split(/\s+/)
372    .map((c) => replaceXRange(c, options))
373    .join(' ')
374}
375
376const replaceXRange = (comp, options) => {
377  comp = comp.trim()
378  const r = options.loose ? re[t.XRANGELOOSE] : re[t.XRANGE]
379  return comp.replace(r, (ret, gtlt, M, m, p, pr) => {
380    debug('xRange', comp, ret, gtlt, M, m, p, pr)
381    const xM = isX(M)
382    const xm = xM || isX(m)
383    const xp = xm || isX(p)
384    const anyX = xp
385
386    if (gtlt === '=' && anyX) {
387      gtlt = ''
388    }
389
390    // if we're including prereleases in the match, then we need
391    // to fix this to -0, the lowest possible prerelease value
392    pr = options.includePrerelease ? '-0' : ''
393
394    if (xM) {
395      if (gtlt === '>' || gtlt === '<') {
396        // nothing is allowed
397        ret = '<0.0.0-0'
398      } else {
399        // nothing is forbidden
400        ret = '*'
401      }
402    } else if (gtlt && anyX) {
403      // we know patch is an x, because we have any x at all.
404      // replace X with 0
405      if (xm) {
406        m = 0
407      }
408      p = 0
409
410      if (gtlt === '>') {
411        // >1 => >=2.0.0
412        // >1.2 => >=1.3.0
413        gtlt = '>='
414        if (xm) {
415          M = +M + 1
416          m = 0
417          p = 0
418        } else {
419          m = +m + 1
420          p = 0
421        }
422      } else if (gtlt === '<=') {
423        // <=0.7.x is actually <0.8.0, since any 0.7.x should
424        // pass.  Similarly, <=7.x is actually <8.0.0, etc.
425        gtlt = '<'
426        if (xm) {
427          M = +M + 1
428        } else {
429          m = +m + 1
430        }
431      }
432
433      if (gtlt === '<') {
434        pr = '-0'
435      }
436
437      ret = `${gtlt + M}.${m}.${p}${pr}`
438    } else if (xm) {
439      ret = `>=${M}.0.0${pr} <${+M + 1}.0.0-0`
440    } else if (xp) {
441      ret = `>=${M}.${m}.0${pr
442      } <${M}.${+m + 1}.0-0`
443    }
444
445    debug('xRange return', ret)
446
447    return ret
448  })
449}
450
451// Because * is AND-ed with everything else in the comparator,
452// and '' means "any version", just remove the *s entirely.
453const replaceStars = (comp, options) => {
454  debug('replaceStars', comp, options)
455  // Looseness is ignored here.  star is always as loose as it gets!
456  return comp
457    .trim()
458    .replace(re[t.STAR], '')
459}
460
461const replaceGTE0 = (comp, options) => {
462  debug('replaceGTE0', comp, options)
463  return comp
464    .trim()
465    .replace(re[options.includePrerelease ? t.GTE0PRE : t.GTE0], '')
466}
467
468// This function is passed to string.replace(re[t.HYPHENRANGE])
469// M, m, patch, prerelease, build
470// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5
471// 1.2.3 - 3.4 => >=1.2.0 <3.5.0-0 Any 3.4.x will do
472// 1.2 - 3.4 => >=1.2.0 <3.5.0-0
473const hyphenReplace = incPr => ($0,
474  from, fM, fm, fp, fpr, fb,
475  to, tM, tm, tp, tpr, tb) => {
476  if (isX(fM)) {
477    from = ''
478  } else if (isX(fm)) {
479    from = `>=${fM}.0.0${incPr ? '-0' : ''}`
480  } else if (isX(fp)) {
481    from = `>=${fM}.${fm}.0${incPr ? '-0' : ''}`
482  } else if (fpr) {
483    from = `>=${from}`
484  } else {
485    from = `>=${from}${incPr ? '-0' : ''}`
486  }
487
488  if (isX(tM)) {
489    to = ''
490  } else if (isX(tm)) {
491    to = `<${+tM + 1}.0.0-0`
492  } else if (isX(tp)) {
493    to = `<${tM}.${+tm + 1}.0-0`
494  } else if (tpr) {
495    to = `<=${tM}.${tm}.${tp}-${tpr}`
496  } else if (incPr) {
497    to = `<${tM}.${tm}.${+tp + 1}-0`
498  } else {
499    to = `<=${to}`
500  }
501
502  return `${from} ${to}`.trim()
503}
504
505const testSet = (set, version, options) => {
506  for (let i = 0; i < set.length; i++) {
507    if (!set[i].test(version)) {
508      return false
509    }
510  }
511
512  if (version.prerelease.length && !options.includePrerelease) {
513    // Find the set of versions that are allowed to have prereleases
514    // For example, ^1.2.3-pr.1 desugars to >=1.2.3-pr.1 <2.0.0
515    // That should allow `1.2.3-pr.2` to pass.
516    // However, `1.2.4-alpha.notready` should NOT be allowed,
517    // even though it's within the range set by the comparators.
518    for (let i = 0; i < set.length; i++) {
519      debug(set[i].semver)
520      if (set[i].semver === Comparator.ANY) {
521        continue
522      }
523
524      if (set[i].semver.prerelease.length > 0) {
525        const allowed = set[i].semver
526        if (allowed.major === version.major &&
527            allowed.minor === version.minor &&
528            allowed.patch === version.patch) {
529          return true
530        }
531      }
532    }
533
534    // Version has a -pre, but it's not one of the ones we like.
535    return false
536  }
537
538  return true
539}
540