Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion classes/comparator.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,9 @@ class Comparator {
(this.value === '<0.0.0-0' || comp.value === '<0.0.0-0')) {
return false
}
const isNullSet = v => v === '<0.0.0' || v === '<0.0.0-0'
if (!options.includePrerelease &&
(this.value.startsWith('<0.0.0') || comp.value.startsWith('<0.0.0'))) {
(isNullSet(this.value) || isNullSet(comp.value))) {
return false
}

Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/comparator-intersection.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,13 @@ module.exports = [
['<0.1.0', '<0.0.0-0', false],
['<0.0.0-0', '<0.1.0', false, true],
['<0.1.0', '<0.0.0-0', false, true],
// A `<0.0.0-<id>` comparator (with a non-minimal prerelease) is NOT the empty
// set: it matches prereleases below `<id>` such as `0.0.0-0`, so it can
// intersect other ranges. Only `<0.0.0` and `<0.0.0-0` match nothing. See #521.
['<0.0.0-rc.1', '>=0.0.0-alpha.0', true],
['<0.0.0-rc.1', '>0.0.0-alpha.0', true],
['<0.0.0-beta', '>=0.0.0-alpha', true],
['<0.0.0-alpha', '<0.0.0-rc.1', true],
['<0.0.0', '>=0.0.0-alpha.0', false],
['<0.0.0', '>0.0.0-alpha.0', false],
]
6 changes: 6 additions & 0 deletions test/fixtures/range-intersection.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,10 @@ module.exports = [
['1.x', '1.3.0 || <1.0.0 >2.0.0', true],
['*', '*', true],
['x', '', true],
// `<0.0.0-<id>` matches prereleases below `<id>`, so it can intersect ranges
// that also admit those prereleases; only `<0.0.0`/`<0.0.0-0` are empty. #521
['<0.0.0-rc.1', '~0.0.0-alpha.0', true],
['<0.0.0-rc.1 <=0.3.2-0', '~0.0.0-alpha.0 || 1', true],
['<0.0.0', '0.x', false],
['<0.0.0', '>=0.0.0-alpha.0', false],
]
49 changes: 49 additions & 0 deletions test/ranges/intersects.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const { test } = require('tap')
const intersects = require('../../ranges/intersects')
const satisfies = require('../../functions/satisfies')
const Range = require('../../classes/range')
const Comparator = require('../../classes/comparator')
const comparatorIntersection = require('../fixtures/comparator-intersection.js')
Expand Down Expand Up @@ -58,3 +59,51 @@ test('missing comparator parameter in intersect comparators', (t) => {
'throws type error')
t.end()
})

// Differential oracle: if a concrete version satisfies BOTH ranges, then the
// ranges MUST intersect. This densely samples the near-zero / prerelease space
// where `<0.0.0-<id>` comparators live, guarding against the over-broad
// `startsWith('<0.0.0')` empty-range check that wrongly treated e.g.
// `<0.0.0-rc.1` (which matches `0.0.0-0`, `0.0.0-alpha`, ...) as matching
// nothing, producing false negatives from `intersects`.
test('intersects agrees with pointwise satisfaction', t => {
const nums = [0, 1, 2]
const pres = ['', '-0', '-alpha', '-alpha.0', '-alpha.1', '-beta', '-rc.1', '-rc.2']
const grid = []
for (const a of nums) {
for (const b of nums) {
for (const c of nums) {
for (const p of pres) {
grid.push(`${a}.${b}.${c}${p}`)
}
}
}
}

const anchors = ['0.0.0', '0.0.0-0', '0.0.0-alpha', '0.0.0-rc.1',
'0.1.0-alpha', '1.0.0-alpha', '1.0.0']
const comparators = []
for (const op of ['<', '<=', '>', '>=', '']) {
for (const v of anchors) {
comparators.push(`${op}${v}`)
}
}

let witnessBacked = 0
for (const opts of [{}, { includePrerelease: true }]) {
for (const a of comparators) {
for (const b of comparators) {
const witness = grid.find(v => satisfies(v, a, opts) && satisfies(v, b, opts))
if (witness === undefined) {
continue
}
witnessBacked++
const label = `${JSON.stringify(opts)}: ${witness} satisfies both`
t.equal(intersects(a, b, opts), true, `${a} ∩ ${b} (${label})`)
t.equal(intersects(b, a, opts), true, `${b} ∩ ${a} symmetry (${label})`)
}
}
}
t.ok(witnessBacked > 100, `exercised ${witnessBacked} witness-backed pairs`)
t.end()
})