From 33b46304e23ce7f8a4d803d550002deeef8a856a Mon Sep 17 00:00:00 2001 From: Yarchik Date: Thu, 23 Jul 2026 14:05:30 +0100 Subject: [PATCH] fix: subset false-positive with a prerelease eq and a differing bound MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `subset(sub, dom)` returned `true` when `sub` combined an exact prerelease comparator (`=X.Y.Z-pre`) with a `>`/`>=`/`<`/`<=` bound of a different `[major,minor,patch]` tuple, even though `sub` contains a version outside `dom`: subset('=1.1.2-alpha <3.1.0', '<1.0.0') // true, must be false satisfies('1.1.2-alpha', '=1.1.2-alpha <3.1.0') // true (in sub) satisfies('1.1.2-alpha', '<1.0.0') // false (not in dom) In `simpleSubset`, the eqSet-vs-bound checks used `satisfies(eq, String(gt), options)`, which rebuilds a full Range and re-applies node-semver's prerelease-exclusion gating, so a prerelease `eq` is judged not to satisfy a plain bound of another tuple. The code then treats the eqSet as inconsistent and returns `null` (null set), which `subset` reports as a subset of everything. Test the eq version against the raw bound comparator instead (`gt.test(eq)` / `lt.test(eq)`) — the same fix PR #867 applied to the dom-side checks, which this left in place on the eqSet side. --- ranges/subset.js | 8 ++++++-- test/ranges/subset.js | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ranges/subset.js b/ranges/subset.js index a9498323..45efaeab 100644 --- a/ranges/subset.js +++ b/ranges/subset.js @@ -124,11 +124,15 @@ const simpleSubset = (sub, dom, options) => { // will iterate one or zero times for (const eq of eqSet) { - if (gt && !satisfies(eq, String(gt), options)) { + // test the eq version against the raw bound comparator; going through + // satisfies() rebuilds a full Range and re-applies prerelease gating, which + // wrongly rejects a prerelease eq against a plain bound of another tuple + // (the same fix PR #867 applied to the dom-side checks below) + if (gt && !gt.test(eq)) { return null } - if (lt && !satisfies(eq, String(lt), options)) { + if (lt && !lt.test(eq)) { return null } diff --git a/test/ranges/subset.js b/test/ranges/subset.js index c6de3570..a0781be1 100644 --- a/test/ranges/subset.js +++ b/test/ranges/subset.js @@ -13,6 +13,11 @@ const cases = [ ['1.2.3', '>1.2.0', true], ['1.2.3 2.3.4 || 2.3.4', '3', false], ['^1.2.3-pre.0', '1.x', false], + // a prerelease `=` comparator combined with a bound of a different tuple must + // not be treated as a null set (subset false-positive): 1.1.2-alpha is in sub + // but not in dom, so sub is not a subset of dom + ['=1.1.2-alpha <3.1.0', '<1.0.0', false], + ['<3.1.0-0 1.1.2-alpha', '~2.0', false], ['^1.2.3-pre.0', '1.x', true, { includePrerelease: true }], ['>2 <1', '3', true], ['1 || 2 || 3', '>=1.0.0', true],