diff --git a/ranges/subset.js b/ranges/subset.js index a9498323..7c893d83 100644 --- a/ranges/subset.js +++ b/ranges/subset.js @@ -23,6 +23,9 @@ const compare = require('../functions/compare.js') // - If EQ is more than one, return true (null set) // - Let GT be the highest > or >= comparator in c // - Let LT be the lowest < or <= comparator in c +// - If LT is set without GT or EQ, use the lowest admitted version for GT +// - When not in prerelease mode, replace an exclusive stable LT ` LT.semver, return true (null set) // - If any C is a = range, and GT or LT are set, return false // - If EQ @@ -49,9 +52,9 @@ const subset = (sub, dom, options = {}) => { sub = new Range(sub, options) dom = new Range(dom, options) - let sawNonNull = false OUTER: for (const simpleSub of sub.set) { + let sawNonNull = false for (const simpleDom of dom.set) { const isSub = simpleSubset(simpleSub, simpleDom, options) sawNonNull = sawNonNull || isSub !== null @@ -112,6 +115,19 @@ const simpleSubset = (sub, dom, options) => { return null } + if (lt && !gt && eqSet.size === 0) { + // An omitted lower bound still starts at the lowest admitted version. + const includesZeroPrerelease = options.includePrerelease || + (lt.semver.prerelease.length && lt.semver.major === 0 && + lt.semver.minor === 0 && lt.semver.patch === 0) + gt = (includesZeroPrerelease ? minimumVersionWithPreRelease : minimumVersion)[0] + + // No prereleases of this tuple are admitted by an exclusive stable bound. + if (!options.includePrerelease && lt.operator === '<' && !lt.semver.prerelease.length) { + lt = new Comparator(`${lt.value}-0`, options) + } + } + let gtltComp if (gt && lt) { gtltComp = compare(gt.semver, lt.semver, options) @@ -142,7 +158,7 @@ const simpleSubset = (sub, dom, options) => { } let higher, lower - let hasDomLT, hasDomGT + let hasDomLT // if the subset has a prerelease, we need a comparator in the superset // with the same tuple and a prerelease, or it's not a subset let needDomLTPre = lt && @@ -157,26 +173,25 @@ const simpleSubset = (sub, dom, options) => { needDomLTPre = false } + // EQ cases returned above; ANY and LT-only ranges now have a lower bound, + // so gt is always set here. for (const c of dom) { - hasDomGT = hasDomGT || c.operator === '>' || c.operator === '>=' hasDomLT = hasDomLT || c.operator === '<' || c.operator === '<=' - if (gt) { - if (needDomGTPre) { - if (c.semver.prerelease && c.semver.prerelease.length && - c.semver.major === needDomGTPre.major && - c.semver.minor === needDomGTPre.minor && - c.semver.patch === needDomGTPre.patch) { - needDomGTPre = false - } + if (needDomGTPre) { + if (c.semver.prerelease && c.semver.prerelease.length && + c.semver.major === needDomGTPre.major && + c.semver.minor === needDomGTPre.minor && + c.semver.patch === needDomGTPre.patch) { + needDomGTPre = false } - if (c.operator === '>' || c.operator === '>=') { - higher = higherGT(gt, c, options) - if (higher === c && higher !== gt) { - return false - } - } else if (gt.operator === '>=' && !c.test(gt.semver)) { + } + if (c.operator === '>' || c.operator === '>=') { + higher = higherGT(gt, c, options) + if (higher === c && higher !== gt) { return false } + } else if (gt.operator === '>=' && !c.test(gt.semver)) { + return false } if (lt) { if (needDomLTPre) { @@ -208,10 +223,6 @@ const simpleSubset = (sub, dom, options) => { return false } - if (lt && hasDomGT && !gt && gtltComp !== 0) { - return false - } - // we needed a prerelease range in a specific tuple, but didn't get one // then this isn't a subset. eg >=1.2.3-pre is not a subset of >=1.0.0, // because it includes prereleases in the 1.2.3 tuple diff --git a/test/ranges/subset.js b/test/ranges/subset.js index c6de3570..8669a3e3 100644 --- a/test/ranges/subset.js +++ b/test/ranges/subset.js @@ -24,6 +24,51 @@ const cases = [ ['^1.2.3-pre.0', '*', true, { includePrerelease: true }], ['1 || 2 || 3', '*', true], + // upper-bound-only ranges start at the lowest admitted version + ['<2.0.0', '*', true], + ['<2', '*', true], + ['<2.0.0', '>=0.0.0', true], + ['<2.0.0', '>=0.0.0-0', true], + ['<1.0.0', '0.x', true], + ['<01.0.0', '0.x', true, { loose: true }], + ['<=2.0.0', '*', true], + ['<2.0.0', '>0.0.0', false], + ['<2.0.0', '>=0.0.1', false], + ['<2.0.0', '>=1.0.0', false], + ['<2.0.0', '*', true, { includePrerelease: true }], + ['<2.0.0', '>=0.0.0', false, { includePrerelease: true }], + ['<2.0.0', '>0.0.0-0', false, { includePrerelease: true }], + ['<2.0.0', '>=0.0.0-0', true, { includePrerelease: true }], + ['<1.0.0', '0.x', false, { includePrerelease: true }], + ['<=1.0.0', '0.x', false], + ['<1.0.0', '<1.0.0-0', true], + ['<1.0.1', '0.x', false], + ['<1.0.0-0', '0.x', true], + ['<1.0.0-beta', '<1.0.0-beta.2', true], + ['>=1.0.0-alpha <1.0.0', '0.x', false], + ['<0.0.0-beta', '*', false], + ['<0.0.0-beta', '>=0.0.0-0', true], + ['<0.0.0-beta', '>0.0.0-0', false], + ['<0.0.1-beta', '>=0.0.0-0', false], + ['<0.1.0-beta', '>=0.0.0-0', false], + ['<0.0.0-0', '1.0.0', true], + ['<0.0.0', '1.0.0', true], + ['<=0.0.0', '0.0.0', true], + + // empty upper-bound-only branches do not change a union + ['<1.0.0 || <0.0.0', '<2.0.0', true], + ['<0.0.0 || <1.0.0', '<2.0.0', true], + ['<1.0.0 || <0.0.0', '<0.5.0 || <2.0.0', true], + ['<0.0.0 || <1.0.0', '<0.5.0 || <2.0.0', true], + ['<0.0.0 || <1.0.0 || <0.0.0', '<2.0.0', true], + ['<1.0.0 || <0.0.0', '<0.5.0', false], + ['<0.0.0 || <1.0.0', '<0.5.0', false], + ['<0.0.0 || <0.0.0', '1.0.0', true], + ['<=0.0.0 || <0.0.0', '0.0.0', true], + ['<0.0.0 || <=0.0.0', '0.0.0', true], + ['<01.0.0 || <00.0.0', '<2.0.0', true, { loose: true }], + ['<1.0.0 || <0.0.0', '>=0.0.0 <2.0.0', false, { includePrerelease: true }], + // prerelease edge cases ['^1.2.3-pre.0', '>=1.0.0', false], ['^1.2.3-pre.0', '>=1.0.0', true, { includePrerelease: true }], @@ -82,6 +127,7 @@ const cases = [ ['>=1.0.0 <2.0.0', '<2.0.0', true], ['>=1.0.0 <2.0.0', '>0.0.0', true], ['>=1.0.0 <=1.0.0', '1.0.0', true], + ['>=1.0.0 <=2.0.0', '1.0.0', false], ['>=1.0.0 <=1.0.0', '2.0.0', false], ['<2.0.0', '>=1.0.0 <2.0.0', false], ['>=1.0.0', '>=1.0.0 <2.0.0', false],