diff --git a/semver/_comparator_intersects.ts b/semver/_comparator_intersects.ts index f5df665dd656..72cbba2766a6 100644 --- a/semver/_comparator_intersects.ts +++ b/semver/_comparator_intersects.ts @@ -1,9 +1,9 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import type { Comparator } from "./types.ts"; -import { greaterOrEqual } from "./greater_or_equal.ts"; -import { lessOrEqual } from "./less_or_equal.ts"; -import { comparatorMin } from "./_comparator_min.ts"; -import { comparatorMax } from "./_comparator_max.ts"; +import { compare } from "./compare.ts"; +import { testRange } from "./test_range.ts"; +import { isWildcardComparator } from "./_shared.ts"; + /** * Returns true if the range of possible versions intersects with the other comparators set of possible versions * @param c0 The left side comparator @@ -14,41 +14,41 @@ export function comparatorIntersects( c0: Comparator, c1: Comparator, ): boolean { - const l0 = comparatorMin(c0); - const l1 = comparatorMax(c0); - const r0 = comparatorMin(c1); - const r1 = comparatorMax(c1); + const op0 = c0.operator; + const op1 = c1.operator; + + if (op0 === "" || op0 === undefined) { + // if c0 is empty comparator, then returns true + if (isWildcardComparator(c0)) { + return true; + } + return testRange(c0, [[c1]]); + } else if (op1 === "" || op1 === undefined) { + if (isWildcardComparator(c1)) { + return true; + } + return testRange(c1, [[c0]]); + } + + const cmp = compare(c0, c1); + + const sameDirectionIncreasing = (op0 === ">=" || op0 === ">") && + (op1 === ">=" || op1 === ">"); + const sameDirectionDecreasing = (op0 === "<=" || op0 === "<") && + (op1 === "<=" || op1 === "<"); + const sameSemVer = cmp === 0; + const differentDirectionsInclusive = (op0 === ">=" || op0 === "<=") && + (op1 === ">=" || op1 === "<="); + const oppositeDirectionsLessThan = cmp === -1 && + (op0 === ">=" || op0 === ">") && + (op1 === "<=" || op1 === "<"); + const oppositeDirectionsGreaterThan = cmp === 1 && + (op0 === "<=" || op0 === "<") && + (op1 === ">=" || op1 === ">"); - // We calculate the min and max ranges of both comparators. - // The minimum min is 0.0.0, the maximum max is ANY. - // - // Comparators with equality operators have the same min and max. - // - // We then check to see if the min's of either range falls within the span of the other range. - // - // A couple of intersection examples: - // ``` - // l0 ---- l1 - // r0 ---- r1 - // ``` - // ``` - // l0 ---- l1 - // r0 ---- r1 - // ``` - // ``` - // l0 ------ l1 - // r0--r1 - // ``` - // ``` - // l0 - l1 - // r0 - r1 - // ``` - // - // non-intersection example - // ``` - // l0 -- l1 - // r0 -- r1 - // ``` - return (greaterOrEqual(l0, r0) && lessOrEqual(l0, r1)) || - (greaterOrEqual(r0, l0) && lessOrEqual(r0, l1)); + return sameDirectionIncreasing || + sameDirectionDecreasing || + (sameSemVer && differentDirectionsInclusive) || + oppositeDirectionsLessThan || + oppositeDirectionsGreaterThan; } diff --git a/semver/_shared.ts b/semver/_shared.ts index 2543ca23b98d..f31f5366e9d5 100644 --- a/semver/_shared.ts +++ b/semver/_shared.ts @@ -1,5 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { type Comparator } from "./types.ts"; + export function compareNumber( a: number, b: number, @@ -188,3 +190,11 @@ export function parseNumber(input: string, errorMessage: string) { if (!isValidNumber(number)) throw new TypeError(errorMessage); return number; } + +export function isWildcardComparator(c: Comparator): boolean { + return ( + Number.isNaN(c.major) && Number.isNaN(c.minor) && Number.isNaN(c.patch) && + (c.prerelease === undefined || c.prerelease.length === 0) && + (c.build === undefined || c.build.length === 0) + ); +} diff --git a/semver/range_intersects_test.ts b/semver/range_intersects_test.ts index dd24b55459bf..65b2fb6a59ba 100644 --- a/semver/range_intersects_test.ts +++ b/semver/range_intersects_test.ts @@ -29,6 +29,14 @@ Deno.test({ ], [">=1.0.0", "<=1.0.0", true], [">1.0.0 <1.0.0", "<=0.0.0", false], + // Pre-release ranges + ["<1.0.0", ">1.0.0-5", true], + [">1.0.0", "<1.0.0-5", false], + [">1.0.0-2", "<=1.0.0-5", true], + [">=1.0.0-2", "<1.0.0-5", true], + ["<7.0.0-beta.20", ">7.0.0-beta.0", true], + ["<7.0.0-beta.beta", ">7.0.0-beta.alpha", true], + // Wildcards ["*", "0.0.1", true], ["*", ">=1.0.0", true], ["*", ">1.0.0", true], diff --git a/semver/range_min_test.ts b/semver/range_min_test.ts index c71c257dc9a3..2d57862fc82c 100644 --- a/semver/range_min_test.ts +++ b/semver/range_min_test.ts @@ -55,8 +55,8 @@ Deno.test({ [">4 || <2", "0.0.0"], ["<=2 || >=4", "0.0.0"], [">=4 || <=2", "0.0.0"], - ["<0.0.0-beta >0.0.0-alpha", INVALID], - [">0.0.0-alpha <0.0.0-beta", INVALID], + ["<0.0.0-beta >=0.0.0-alpha", "0.0.0-alpha"], + [">=0.0.0-alpha <0.0.0-beta", "0.0.0-alpha"], // Greater than or equal [">=1.1.1 <2 || >=2.2.2 <2", "1.1.1"], diff --git a/semver/test_range.ts b/semver/test_range.ts index 047e94991097..92094b828406 100644 --- a/semver/test_range.ts +++ b/semver/test_range.ts @@ -1,9 +1,76 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import type { Range, SemVer } from "./types.ts"; -import { greaterOrEqual } from "./greater_or_equal.ts"; -import { lessOrEqual } from "./less_or_equal.ts"; -import { comparatorMin } from "./_comparator_min.ts"; -import { comparatorMax } from "./_comparator_max.ts"; +import type { Comparator, Range, SemVer } from "./types.ts"; +import { compare } from "./compare.ts"; +import { isWildcardComparator } from "./_shared.ts"; + +function testComparator(version: SemVer, comparator: Comparator): boolean { + if (isWildcardComparator(comparator)) { + return true; + } + const cmp = compare(version, comparator.semver ?? comparator); + switch (comparator.operator) { + case "": + case "=": + case "==": + case "===": + case undefined: { + return cmp === 0; + } + case "!=": + case "!==": { + return cmp !== 0; + } + case ">": { + return cmp > 0; + } + case "<": { + return cmp < 0; + } + case ">=": { + return cmp >= 0; + } + case "<=": { + return cmp <= 0; + } + } +} + +function testComparatorSet( + version: SemVer, + set: Comparator[], +): boolean { + for (const comparator of set) { + if (!testComparator(version, comparator)) { + return false; + } + } + if (version.prerelease && version.prerelease.length > 0) { + // Find the comparator that is allowed to have prereleases + // For example, ^1.2.3-pr.1 desugars to >=1.2.3-pr.1 <2.0.0 + // That should allow `1.2.3-pr.2` to pass. + // However, `1.2.4-alpha.notready` should NOT be allowed, + // even though it's within the range set by the comparators. + for (const comparator of set) { + if (isWildcardComparator(comparator)) { + continue; + } + const { prerelease } = comparator.semver ?? comparator; + if (prerelease && prerelease.length > 0) { + const major = comparator.semver?.major ?? comparator.major; + const minor = comparator.semver?.minor ?? comparator.minor; + const patch = comparator.semver?.patch ?? comparator.patch; + if ( + version.major === major && version.minor === minor && + version.patch === patch + ) { + return true; + } + } + } + return false; + } + return true; +} /** * Test to see if the version satisfies the range. @@ -15,15 +82,5 @@ export function testRange( version: SemVer, range: Range, ): boolean { - for (const r of range) { - if ( - r.every((c) => - greaterOrEqual(version, comparatorMin(c)) && - lessOrEqual(version, comparatorMax(c)) - ) - ) { - return true; - } - } - return false; + return range.some((set) => testComparatorSet(version, set)); } diff --git a/semver/test_range_test.ts b/semver/test_range_test.ts index eb13f708b498..0f2afc97f374 100644 --- a/semver/test_range_test.ts +++ b/semver/test_range_test.ts @@ -5,7 +5,7 @@ import { parse } from "./parse.ts"; import { parseRange } from "./parse_range.ts"; import { testRange } from "./test_range.ts"; -Deno.test("range", async (t) => { +Deno.test("testRange() returns true when the version is in the range", async (t) => { const versions: [string, string][] = [ ["1.0.0 - 2.0.0", "1.2.3"], ["^1.2.3+build", "1.2.3"], @@ -13,7 +13,6 @@ Deno.test("range", async (t) => { ["1.2.3-pre+asdf - 2.4.3-pre+asdf", "1.2.3"], ["1.2.3-pre+asdf - 2.4.3-pre+asdf", "1.2.3-pre.2"], ["1.2.3-pre+asdf - 2.4.3-pre+asdf", "2.4.3-alpha"], - ["1.2.3+asdf - 2.4.3+asdf", "2.4.3-alpha"], ["1.2.3+asdf - 2.4.3+asdf", "2.4.3"], ["1.0.0", "1.0.0"], [">=*", "0.2.4"], @@ -71,7 +70,6 @@ Deno.test("range", async (t) => { ["1.2.3 >=1.2.1", "1.2.3"], [">=1.2.3 >=1.2.1", "1.2.3"], [">=1.2.1 >=1.2.3", "1.2.3"], - ["<=1.2.3", "1.2.3-beta"], [">=1.2", "1.2.8"], ["^1.2.3", "1.8.1"], ["^0.1.2", "0.1.2"], @@ -102,7 +100,7 @@ Deno.test("range", async (t) => { }); Deno.test({ - name: "negativeRange", + name: "testRange() returns false when the version is not in the range", fn: async (t) => { const versions: [string, string][] = [ ["1.0.0 - 2.0.0", "2.2.3"], @@ -110,6 +108,7 @@ Deno.test({ ["^1.2.3+build", "2.0.0"], ["^1.2.3+build", "1.2.0"], ["1.2.3-pre+asdf - 2.4.3-pre+asdf", "2.4.3"], + ["1.2.3+asdf - 2.4.3+asdf", "2.4.3-alpha"], ["^1.2.3", "1.2.3-pre"], ["^1.2", "1.2.0-pre"], [">1.2", "1.3.0-beta"], @@ -169,6 +168,14 @@ Deno.test({ ["^1.2.3", "1.2.2"], ["^1.2", "1.1.9"], + // unsatisfiable patterns with prereleases + ["*", "1.0.0-rc1"], + ["^1.0.0-0", "1.0.1-rc1"], + ["^1.0.0-rc2", "1.0.1-rc1"], + ["^1.0.0", "1.0.1-rc1"], + ["^1.0.0", "1.1.0-rc1"], + ["<=1.2.3", "1.2.3-beta"], + // invalid ranges never satisfied! ["blerg", "1.2.3"], ["^1.2.3", "2.0.0-pre"], @@ -185,28 +192,11 @@ Deno.test({ }, }); -Deno.test("unlockedPrereleaseRange", function () { - const versions: [string, string][] = [ - ["*", "1.0.0-rc1"], - ["^1.0.0-0", "1.0.1-rc1"], - ["^1.0.0-rc2", "1.0.1-rc1"], - ["^1.0.0", "1.0.1-rc1"], - ["^1.0.0", "1.1.0-rc1"], - ]; - - for (const [r, v] of versions) { - const range = parseRange(r); - const s = parse(v); - const found = testRange(s, range); - assert(found, `${r} not satisfied by ${v}`); - } -}); - Deno.test("negativeUnlockedPrereleaseRange", function () { const versions: [string, string][] = [ ["^1.0.0", "1.0.0-rc1"], ["^1.2.3-rc2", "2.0.0"], - ["^1.0.0", "2.0.0-rc1"], // todo: review, this is inverted from original test case + ["^1.0.0", "2.0.0-rc1"], ]; for (const [r, v] of versions) {