Skip to content

fix: keep the Comparator options when building a Range - #935

Open
MFA-G wants to merge 1 commit into
npm:mainfrom
MFA-G:fix/range-from-comparator-options
Open

MFA-G wants to merge 1 commit into
npm:mainfrom
MFA-G:fix/range-from-comparator-options

Conversation

@MFA-G

@MFA-G MFA-G commented Oct 29, 2026

Copy link
Copy Markdown

What changed

new Range(comparator, options) returns from the range instanceof Comparator branch before this.options, this.loose and this.includePrerelease are assigned, so those fields stayed undefined on a comparator-built range. Range#test() passes this.options to testSet(), which reads options.includePrerelease, so testing any prerelease version against such a range threw:

const semver = require('semver')

const r = new semver.Range(new semver.Comparator('>=1.0.0-alpha'))
r.test('1.0.0')        // true
r.test('1.0.0-beta')   // TypeError: Cannot read properties of undefined (reading 'includePrerelease')

// satisfies() takes the same path and threw too
semver.satisfies('1.0.0-beta', new semver.Comparator('>=1.0.0-alpha'))

Stable versions happened to work because testSet() short-circuits on version.prerelease.length.

Fixes #908.

Fix

Assign the three fields in the Comparator branch. A comparator already carries the options it was parsed with, so those are inherited when the caller passes no options; explicit options still win:

  • new Range(comparator) uses the comparator's options (including includePrerelease).
  • new Range(comparator, { includePrerelease: true }) and new Range(comparator, true) (loose) use what the caller asked for.

A comparator-built range now behaves exactly like the equivalent string range for .test(), .range, loose and includePrerelease.

Tests

Added to test/classes/range.js:

  • a comparator-built range can test prerelease and stable versions;
  • includePrerelease is inherited from the comparator, and an explicit option overrides it;
  • boolean options (loose) are still parsed;
  • options/loose/includePrerelease/range are populated.

Validation

  • npx tap (full suite, coverage thresholds): pass, 53 test files, no failures
  • npx eslint classes/range.js test/classes/range.js: clean

`new Range(comparator, options)` returned from the `instanceof Comparator`
branch before assigning `this.options`, `this.loose` and
`this.includePrerelease`, so those fields stayed `undefined`. As a result
`range.test()` threw a TypeError for any prerelease version (it reads
`options.includePrerelease` in `testSet`) and `range.range`/`toString()`
failed the same way. `satisfies(version, comparator)` went through the
same path and threw as well.

Assign the missing fields in that branch. A comparator already carries the
options it was parsed with, so they are inherited when the caller does not
pass any; explicit options still win. This makes a comparator-built range
behave like the equivalent string range.

Fixes npm#908
@MFA-G
MFA-G requested a review from a team as a code owner October 29, 2026 01:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] new Range(comparator).test() throws TypeError for a prerelease version

1 participant