Skip to content

fix: intersects() false-negative for <0.0.0-<prerelease> comparators#885

Open
gaoflow wants to merge 1 commit into
npm:mainfrom
gaoflow:fix-intersects-prerelease-null-set
Open

fix: intersects() false-negative for <0.0.0-<prerelease> comparators#885
gaoflow wants to merge 1 commit into
npm:mainfrom
gaoflow:fix-intersects-prerelease-null-set

Conversation

@gaoflow

@gaoflow gaoflow commented Jul 4, 2026

Copy link
Copy Markdown

Bug

intersects() returns false for two ranges that demonstrably share a version, contradicting satisfies():

semver.intersects('<0.0.0-rc.1', '>=0.0.0-alpha.0')  // => false  (WRONG)
semver.satisfies('0.0.0-alpha.0', '<0.0.0-rc.1')      // => true
semver.satisfies('0.0.0-alpha.0', '>=0.0.0-alpha.0')  // => true

0.0.0-alpha.0 satisfies both ranges, so they intersect. The tell that it is specific to the 0.0.0 tuple: intersects('<1.0.0-rc', '>=1.0.0-alpha') is correctly true.

Root cause

In Comparator.intersects (classes/comparator.js), the default-mode "this comparator matches nothing" guard uses startsWith('<0.0.0'):

if (!options.includePrerelease &&
  (this.value.startsWith('<0.0.0') || comp.value.startsWith('<0.0.0'))) {
  return false
}

But the only genuinely-empty comparator values are <0.0.0 and the codebase's own null-set sentinel <0.0.0-0 (see range.js: isNullSet = c => c.value === '<0.0.0-0'). <0.0.0-rc.1, <0.0.0-alpha, etc. are not empty — they match prereleases below their own id (0.0.0-0, 0.0.0-alpha, ...). startsWith over-matches them and short-circuits intersects to false.

Fix

Only treat the two real null-set values as empty; everything else falls through to the normal opposite-direction logic. The includePrerelease branch above (already === '<0.0.0-0') is untouched.

Tests

test/ranges/intersects.js gains a property-oracle block that builds a near-zero + prerelease version grid × a comparator matrix and asserts, for every pair with a real shared witness version, that intersects is true in both directions (1,289 witness-backed pairs). Fixture rows are added to comparator-intersection.js (incl. the <0.0.0 guards that must stay false) and range-intersection.js. Full suite passes; lint clean.

Relation to #884

This is independent of open #884. #884 patches set-level decomposition in range.js for cases where one set pins an exact =x.y.z; it does not touch comparator.js. Cross-applied: with #884 alone my <0.0.0-rc.1 case stays false; with this fix alone #884's ^1.2.3-alpha case stays false. If #884 lands first, the only overlap is the appended rows in test/fixtures/range-intersection.js (a trivial append-only rebase).

@gaoflow gaoflow requested a review from a team as a code owner July 4, 2026 21:47
@gaoflow gaoflow changed the title fix: intersects() false-negative for scheme-less <0.0.0-<prerelease> comparators fix: intersects() false-negative for <0.0.0-<prerelease> comparators Jul 4, 2026
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.

1 participant