fix: reject loose ranges that collapse to match everything

In loose mode the comparators that fail to parse are filtered out of a
comparator list, and whatever is left becomes the range. When the only
survivor is the any comparator, a typo such as `* (4.1.x  4.1.2)` drops
`(4.1.x` and `4.1.2)` and collapses to the leftover `*`, so the range
silently satisfies every version instead of being rejected. Without
`loose` the same input throws `Invalid comparator: (4.1.x`.

Treat that comparator list as invalid instead, the same way a list with
no valid comparator at all is already treated: the list is dropped, so
other `||` branches still stand on their own and a range left with no
valid branch throws `Invalid SemVer Range`.

Junk sitting next to a comparator that does parse is still tolerated, so
`>=1.2.3 &&&` still yields `>=1.2.3` and `== 1.0.0` still yields
`1.0.0`.

Fixes #387
This commit is contained in:
dualfroz committed 2026-09-04 22:16:53 +02:00
1 parent d4354cc38b
commit 2124e4b155
3 files changed
+37 -1

No files matched your search

+20
View File
@@ -3,6 +3,7 @@
const { test } = require('tap')
const Range = require('../../classes/range')
const Comparator = require('../../classes/comparator')
const satisfies = require('../../functions/satisfies')
const rangeIntersection = require('../fixtures/range-intersection.js')
const rangeInclude = require('../fixtures/range-include.js')
@@ -38,6 +39,25 @@ test('throw for empty comparator set, even in loose mode', t => {
t.end()
})
test('loose mode does not drop comparators down to a match-everything range', t => {
// `(4.1.x` and `4.1.2)` used to be filtered out, collapsing the range to the
// `*` that was left over, so a typo silently matched every version
t.throws(() => new Range('* (4.1.x 4.1.2)', { loose: true }),
TypeError('Invalid SemVer Range: * (4.1.x 4.1.2)'))
t.equal(satisfies('9.9.9', '* (4.1.x 4.1.2)', { loose: true }), false,
'typo range does not match an unrelated version')
t.equal(satisfies('4.1.2', '* (4.1.x 4.1.2)', { loose: true }), false,
'typo range does not match the version it looks like it means')
// only the collapse to `*` is rejected. loose mode still tolerates junk
// sitting next to a comparator that does parse, and still tolerates a `||`
// branch that is invalid as a whole
t.equal(new Range('>=1.2.3 &&&', { loose: true }).range, '>=1.2.3',
'junk beside a valid comparator is still dropped')
t.equal(new Range('1.2.3 || (foo)', { loose: true }).range, '1.2.3',
'wholly invalid union branch is still tolerated')
t.end()
})
test('convert comparator to range', t => {
const c = new Comparator('>=1.2.3')
const r = new Range(c)