Skip to content

fix: intersects() misses shared prerelease versions#884

Open
MerlijnW70 wants to merge 1 commit into
npm:mainfrom
MerlijnW70:fix/intersects-prerelease-licensing
Open

fix: intersects() misses shared prerelease versions#884
MerlijnW70 wants to merge 1 commit into
npm:mainfrom
MerlijnW70:fix/intersects-prerelease-licensing

Conversation

@MerlijnW70

Copy link
Copy Markdown

Summary

intersects() reports two ranges as non-overlapping even when they demonstrably share a prerelease version. It contradicts satisfies(): both ranges contain the version, yet intersects() says they don't intersect.

Reproduction

const semver = require('semver')

semver.intersects('^1.2.3-alpha', '=1.2.3-alpha')   // => false  (should be true)
semver.satisfies('1.2.3-alpha', '^1.2.3-alpha')      // => true
semver.satisfies('1.2.3-alpha', '=1.2.3-alpha')      // => true

1.2.3-alpha satisfies both ranges, so they clearly intersect. The same false negative occurs for:

semver.intersects('~1.2.3-alpha', '=1.2.3-alpha')             // => false (should be true)
semver.intersects('>=1.2.3-alpha <2.0.0-0', '1.2.3-alpha')    // => false (should be true)

Root cause

Range.intersects (classes/range.js) decomposes the two ranges into independent comparator pairs and requires every pair to intersect:

thisComparators.every((thisComparator) =>
  rangeComparators.every((rangeComparator) =>
    thisComparator.intersects(rangeComparator, options)))

Prerelease licensing, however, is a set-level property. ^1.2.3-alpha desugars to >=1.2.3-alpha <2.0.0-0; the >=1.2.3-alpha comparator is what licenses the prerelease 1.2.3-alpha. When the exact comparator =1.2.3-alpha is paired with <2.0.0-0 in isolation, Comparator.intersects evaluates new Range('<2.0.0-0').test('1.2.3-alpha'), which is false (a single-comparator range applies the prerelease-tuple exclusion). The sibling >=1.2.3-alpha that would license the prerelease is not visible in that pair, so the whole intersection is wrongly reported empty.

Fix

The pairwise check only ever under-reports, so it stays as a sufficient condition. When it fails, recover the missed cases: if one set pins an exact version (a bare =x.y.z), the two sets share a version iff that version satisfies the whole other set — tested with testSet, which applies prerelease licensing across all of a set's comparators.

const comparatorSetsIntersect = (a, b, options) => {
  options = parseOptions(options)
  if (a.every((ac) => b.every((bc) => ac.intersects(bc, options)))) {
    return true
  }
  const aExact = exactVersion(a)
  if (aExact) {
    return testSet(b, aExact, options)
  }
  const bExact = exactVersion(b)
  if (bExact) {
    return testSet(a, bExact, options)
  }
  return false
}

This is deliberately conservative:

  • Ranges with no exact pin fall straight through to the original pairwise result — unchanged.
  • The * / ANY short-circuit is unchanged.
  • A version differential against unmodified 7.8.5 over a matrix of range pairs (with and without includePrerelease) shows the fix flips only the genuine false negatives to true (each backed by a concrete shared version) and changes nothing else.
  • Soundness holds: across the same matrix, every intersects(...) === false still has no version satisfying both ranges.

Tests

Adds fixture cases in test/fixtures/range-intersection.js (exercised in both argument orderings) for the fixed cases and for the still-correct negatives (e.g. =1.2.3-alpha vs <2.0.0, which has no licensing sibling and must remain false). Full suite passes with 100% coverage and lint clean.

Range.intersects() decomposes two ranges into independent comparator
pairs and requires every pair to intersect. That loses set-level
prerelease licensing, so a range whose lower bound licenses a prerelease
was reported as not intersecting an exact match for that prerelease:

    semver.intersects('^1.2.3-alpha', '=1.2.3-alpha')  // false
    semver.satisfies('1.2.3-alpha', '^1.2.3-alpha')    // true
    semver.satisfies('1.2.3-alpha', '=1.2.3-alpha')    // true

Both ranges contain 1.2.3-alpha, yet intersects() reported no overlap.
The same happens for ~ ranges and the desugared '>=1.2.3-alpha <2.0.0-0'
form.

The pairwise test only ever under-reports, so keep it as a sufficient
condition and, when it fails, recover the missed cases: if one set pins
an exact version, the two sets share a version iff that version satisfies
the whole other set (tested with testSet, which applies prerelease
licensing). Ranges without an exact pin are unaffected, and the `*`
short-circuit is preserved.
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