Repository: npm/node-semver (JavaScript)

Issue:
[BUG] `intersects(r1, r2, options)` function returns `true` if the not intersected range `< 0.0.0` and `0.x` are supplied
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

Range `< 0.0.0` (or range `< 0.0.0-0` if the `includePrerelease` option is `true`) should not intersect with all ranges. However, the `intersects(r1, r2, options)` function returns `true` when comparing ranges `< 0.0.0` (`< 0.0.0-0`) and X-Range.

### Expected Behavior

- Returns `false` if compared to range `< 0.0.0` if the `includePrerelease` option is `false`
- Returns `false` if compared to range `< 0.0.0-0` if the `includePrerelease` option is `true`

### Steps To Reproduce

```js
const semver = require('semver'); // Import semver@7.3.8

// This returns `false`, which is as expected since the two ranges do not intersect
semver.intersects('< 1.0.0', '1.0.0', { includePrerelease: false }) === false

// These also return `false`
semver.intersects('< 1.0.0', '1.x', { includePrerelease: false }) === false
semver.intersects('< 0.0.0', '1.x', { includePrerelease: false }) === false
semver.intersects('< 0.0.0', '0.1.x', { includePrerelease: false }) === false
semver.intersects('< 0.0.0', '0.0.0', { includePrerelease: false }) === false

// These return `true` because they intersect if a pre-release is included
semver.intersects('< 1.0.0', '1.x', { includePrerelease: true }) === true
semver.intersects('< 0.0.0', '0.x', { includePrerelease: true }) === true

// However, when compared to the X-Range, this returns `true` instead of `false`
semver.intersects('< 0.0.0', '0.0.x', { includePrerelease: false }) !== false
semver.intersects('< 0.0.0', '0.x', { includePrerelease: false }) !== false
semver.intersects('< 0.0.0', '0', { includePrerelease: false }) !== false
semver.intersects('< 0.0.0-0', '0.0.x', { includePrerelease: true }) !== false
semver.intersects('< 0.0.0-0', '0.x', { includePrerelease: true }) !== false
semver.intersects('< 0.0.0-0', '0', { includePrerelease: true }) !== false

// Whether the range `< 0.0.0` is an X-range or not does not affect the result
semver.intersects('< 0', '0.0.0', { includePrerelease: false }) === false
semver.intersects('< 0.x', '0.0.0', { includePrerelease: false }) === false
semver.intersects('< 0.0.x', '0.0.0', { includePrerelease: false }) === false
semver.intersects('< 0', '0.0.x', { includePrerelease: false }) !== false
semver.intersects('< 0.x', '0.0.x', { includePrerelease: false }) !== false
semver.intersects('< 0.0.x', '0.0.x', { includePrerelease: false }) !== false
```

### Environment

- **semver**: [7.3.8](https://www.npmjs.com/package/semver/v/7.3.8)


Discussion: Technically this should be true if including pre releases, because 0.0.0-0 is less than 0.0.0. 
> Technically this should be true if including pre releases, because 0.0.0-0 is less than 0.0.0.
> https://github.com/npm/node-semver/issues/521#issuecomment-1475325186

If the `includePrerelease` option is true, then yes. But this behavior does not change whether the `includePrerelease` option is `true` or `false`.

![](https://user-images.githubusercontent.com/6049678/226195388-850ada61-d8a3-46a3-afac-625318075f9f.png)

I forgot the `includePrerelease` option existed, so I will add it to the steps to reproduce. Thank you!
Yeah, that definitely seems like a bug to me. Was just noting the edge case so we don't make a new bug when fixing this one 😬 
With `includePrerelease` can anything be less than `0.0.0-0`?
I would assume not; "the rules" say that in prerelease parts, any valid number comes before any valid string, and 0 is the smallest number (negative numbers don't exist 🙈).
Thanks for confirming, that was my understanding also.
If `includePrerelease` is `true` and either range is `<0.0.0-0` we always return `false`
If `includePrerelease` is `false` and either range starts with `<0.0.0` we always return `false`

I think that covers everything?
Found another bug while testing a fix for this:

`semver.intersects('< 0.0.0', '<0.1.x', { includePrerelease: true })`

ends up calling

https://github.com/npm/node-semver/blob/main/ranges/intersects.js#L5

which doesn't pass those options to the `intersects` method itself
nice catch



Fix (patch):
diff --git a/classes/comparator.js b/classes/comparator.js
index 54ce1394..2146c884 100644
--- a/classes/comparator.js
+++ b/classes/comparator.js
@@ -90,32 +90,43 @@ class Comparator {
       return new Range(this.value, options).test(comp.semver)
     }
 
-    const sameDirectionIncreasing =
-      (this.operator === '>=' || this.operator === '>') &&
-      (comp.operator === '>=' || comp.operator === '>')
-    const sameDirectionDecreasing =
-      (this.operator === '<=' || this.operator === '<') &&
-      (comp.operator === '<=' || comp.operator === '<')
-    const sameSemVer = this.semver.version === comp.semver.version
-    const differentDirectionsInclusive =
-      (this.operator === '>=' || this.operator === '<=') &&
-      (comp.operator === '>=' || comp.operator === '<=')
-    const oppositeDirectionsLessThan =
-      cmp(this.semver, '<', comp.semver, options) &&
-      (this.operator === '>=' || this.operator === '>') &&
-        (comp.operator === '<=' || comp.operator === '<')
-    const oppositeDirectionsGreaterThan =
-      cmp(this.semver, '>', comp.semver, options) &&
-      (this.operator === '<=' || this.operator === '<') &&
-        (comp.operator === '>=' || comp.operator === '>')
-
-    return (
-      sameDirectionIncreasing ||
-      sameDirectionDecreasing ||
-      (sameSemVer && differentDirectionsInclusive) ||
-      oppositeDirectionsLessThan ||
-      oppositeDirectionsGreaterThan
-    )
+    options = parseOptions(options)
+
+    // Special cases where nothing can possibly be lower
+    if (options.includePrerelease &&
+      (this.value === '<0.0.0-0' || comp.value === '<0.0.0-0')) {
+      return false
+    }
+    if (!options.includePrerelease &&
+      (this.value.startsWith('<0.0.0') || comp.value.startsWith('<0.0.0'))) {
+      return false
+    }
+
+    // Same direction increasing (> or >=)
+    if (this.operator.startsWith('>') && comp.operator.startsWith('>')) {
+      return true
+    }
+    // Same direction decreasing (< or <=)
+    if (this.operator.startsWith('<') && comp.operator.startsWith('<')) {
+      return true
+    }
+    // same SemVer and both sides are inclusive (<= or >=)
+    if (
+      (this.semver.version === comp.semver.version) &&
+      this.operator.includes('=') && comp.operator.includes('=')) {
+      return true
+    }
+    // opposite directions less than
+    if (cmp(this.semver, '<', comp.semver, options) &&
+      this.operator.startsWith('>') && comp.operator.startsWith('<')) {
+      return true
+    }
+    // opposite directions greater than
+    if (cmp(this.semver, '>', comp.semver, options) &&
+      this.operator.startsWith('<') && comp.operator.startsWith('>')) {
+      return true
+    }
+    return false
   }
 }
 
diff --git a/ranges/intersects.js b/ranges/intersects.js
index 3d1a6f31..e0e9b7ce 100644
--- a/ranges/intersects.js
+++ b/ranges/intersects.js
@@ -2,6 +2,6 @@ const Range = require('../classes/range')
 const intersects = (r1, r2, options) => {
   r1 = new Range(r1, options)
   r2 = new Range(r2, options)
-  return r1.intersects(r2)
+  return r1.intersects(r2, options)
 }
 module.exports = intersects