Skip to content

Commit

Permalink
fix(semver): fix prerelease handlings in range utils (#4323)
Browse files Browse the repository at this point in the history
  • Loading branch information
kt3k authored Feb 16, 2024
1 parent f6c4447 commit 453cc36
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 80 deletions.
80 changes: 40 additions & 40 deletions semver/_comparator_intersects.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
}
10 changes: 10 additions & 0 deletions semver/_shared.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)
);
}
8 changes: 8 additions & 0 deletions semver/range_intersects_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
4 changes: 2 additions & 2 deletions semver/range_min_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
89 changes: 73 additions & 16 deletions semver/test_range.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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));
}
34 changes: 12 additions & 22 deletions semver/test_range_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ 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"],
["^1.2.3+build", "1.3.0"],
["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"],
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -102,14 +100,15 @@ 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"],
["1.2.3+asdf - 2.4.3+asdf", "1.2.3-pre.2"],
["^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"],
Expand Down Expand Up @@ -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"],
Expand All @@ -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) {
Expand Down

0 comments on commit 453cc36

Please sign in to comment.