Skip to content

Commit 01a8dfd

Browse files
authored
refactor(semver): clean up parseRange, add missing tests (#6362)
1 parent 3b75ee7 commit 01a8dfd

File tree

9 files changed

+227
-27
lines changed

9 files changed

+227
-27
lines changed

semver/compare_test.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
2-
import { assertEquals } from "@std/assert";
2+
import { assertEquals, assertThrows } from "@std/assert";
33
import { parse } from "./parse.ts";
44
import { compare } from "./compare.ts";
55

@@ -54,3 +54,50 @@ Deno.test({
5454
}
5555
},
5656
});
57+
58+
Deno.test({
59+
name: "compare() throws on NaN",
60+
fn: () => {
61+
assertThrows(
62+
() =>
63+
compare({ major: NaN, minor: 0, patch: 0 }, {
64+
major: 1,
65+
minor: 0,
66+
patch: 0,
67+
}),
68+
Error,
69+
"Cannot compare against non-numbers",
70+
);
71+
72+
assertThrows(
73+
() =>
74+
compare({ major: 1, minor: 0, patch: 0 }, {
75+
major: 1,
76+
minor: NaN,
77+
patch: 0,
78+
}),
79+
Error,
80+
"Cannot compare against non-numbers",
81+
);
82+
},
83+
});
84+
85+
Deno.test({
86+
name: "compare() handles undefined in prerelease",
87+
fn: () => {
88+
assertEquals(
89+
compare({
90+
major: 1,
91+
minor: 0,
92+
patch: 0,
93+
prerelease: [undefined as unknown as string],
94+
}, {
95+
major: 1,
96+
minor: 0,
97+
patch: 0,
98+
prerelease: [undefined as unknown as string],
99+
}),
100+
0,
101+
);
102+
},
103+
});

semver/greater_than_range_test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22
// Copyright Isaac Z. Schlueter and npm contributors. All rights reserved. ISC license.
33

4-
import { assert, assertFalse } from "@std/assert";
4+
import { assert, assertEquals, assertFalse } from "@std/assert";
55
import {
66
format,
77
formatRange,
88
greaterThanRange,
99
parse,
1010
parseRange,
1111
} from "./mod.ts";
12+
import type { Operator } from "./types.ts";
1213

1314
Deno.test("greaterThanRange() checks if the semver is greater than the range", async (t) => {
1415
// from https://github.com/npm/node-semver/blob/692451bd6f75b38a71a99f39da405c94a5954a22/test/fixtures/version-gt-range.js
@@ -167,3 +168,42 @@ Deno.test("greaterThanRange() checks if the semver is greater than the range", a
167168
});
168169
}
169170
});
171+
172+
Deno.test("greaterThanRange() handles equals operator", () => {
173+
const version = {
174+
major: 1,
175+
minor: 0,
176+
patch: 0,
177+
prerelease: [],
178+
build: [],
179+
};
180+
const range = [[{
181+
operator: "=" as unknown as Operator,
182+
major: 1,
183+
minor: 0,
184+
patch: 0,
185+
prerelease: [],
186+
build: [],
187+
}]];
188+
assertEquals(greaterThanRange(version, range), false);
189+
});
190+
191+
Deno.test("greaterThanRange() handles not equals operator", () => {
192+
const version = {
193+
major: 1,
194+
minor: 0,
195+
patch: 0,
196+
prerelease: [],
197+
build: [],
198+
};
199+
const range = [[{
200+
operator: "!=" as const,
201+
major: 1,
202+
minor: 0,
203+
patch: 0,
204+
prerelease: [],
205+
build: [],
206+
}]];
207+
// FIXME(kt3k): This demonstrates a bug. This should be false
208+
assertEquals(greaterThanRange(version, range), true);
209+
});

semver/is_range_test.ts

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
2-
import { assert } from "@std/assert";
2+
import { assertEquals } from "@std/assert";
33
import { ALL, MIN } from "./_constants.ts";
4-
import { formatRange } from "./format_range.ts";
54
import { isRange } from "./is_range.ts";
65
import type { Range } from "./types.ts";
76

87
Deno.test({
9-
name: "isRange()",
10-
fn: async (t) => {
11-
const ranges: Range[] = [[
12-
[ALL],
13-
], [
8+
name: "isRange() handles simple range",
9+
fn: () => {
10+
const range: Range = [
1411
[{
1512
operator: ">=",
1613
major: 0,
@@ -22,12 +19,53 @@ Deno.test({
2219
operator: "<",
2320
...MIN,
2421
}],
25-
]];
26-
for (const r of ranges) {
27-
await t.step(`${formatRange(r)}`, () => {
28-
const actual = isRange(r);
29-
assert(actual);
30-
});
31-
}
22+
];
23+
const actual = isRange(range);
24+
assertEquals(actual, true);
25+
},
26+
});
27+
28+
Deno.test({
29+
name: "isRange() handles ALL constant",
30+
fn: () => {
31+
const range: Range = [[ALL]];
32+
const actual = isRange(range);
33+
assertEquals(actual, true);
34+
},
35+
});
36+
37+
Deno.test({
38+
name: "isRange() handles null",
39+
fn: () => {
40+
const range: Range = [[null]] as unknown as Range;
41+
const actual = isRange(range);
42+
assertEquals(actual, false);
43+
},
44+
});
45+
46+
Deno.test({
47+
name: "isRange() handles undefined",
48+
fn: () => {
49+
const range: Range = [[undefined]] as unknown as Range;
50+
const actual = isRange(range);
51+
assertEquals(actual, false);
52+
},
53+
});
54+
55+
Deno.test({
56+
name: "isRange() handles array type",
57+
fn: () => {
58+
const range: Range = [[[1, 2, 3]]] as unknown as Range;
59+
const actual = isRange(range);
60+
assertEquals(actual, false);
61+
},
62+
});
63+
64+
Deno.test({
65+
name: "isRange() handles not object type",
66+
fn: () => {
67+
const range: Range = [[[true]]] as unknown as Range;
68+
const actual = isRange(range);
69+
assertEquals(actual, false);
3270
},
3371
});

semver/is_semver_test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22
import { assert } from "@std/assert";
3-
import { MIN } from "./_constants.ts";
3+
import { ANY, MIN } from "./_constants.ts";
44
import { isSemVer } from "./is_semver.ts";
55

66
Deno.test({
@@ -61,6 +61,7 @@ Deno.test({
6161
[{ major: 0, minor: 0, patch: 0, build: [], prerelease: ["abc"] }],
6262
[{ major: 0, minor: 0, patch: 0, build: [], prerelease: ["abc", 0] }],
6363
[MIN],
64+
[ANY],
6465
];
6566
for (const [v] of versions) {
6667
await t.step(

semver/less_than_range_test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22
// Copyright Isaac Z. Schlueter and npm contributors. All rights reserved. ISC license.
33

4-
import { assert, assertFalse } from "@std/assert";
4+
import { assert, assertEquals, assertFalse } from "@std/assert";
55
import {
66
format,
77
formatRange,
@@ -169,3 +169,23 @@ Deno.test("lessThanRange() checks if the SemVer is less than the range", async (
169169
});
170170
}
171171
});
172+
173+
Deno.test("lessThanRange() handles not equals operator", () => {
174+
const version = {
175+
major: 1,
176+
minor: 0,
177+
patch: 0,
178+
prerelease: [],
179+
build: [],
180+
};
181+
const range = [[{
182+
operator: "!=" as const,
183+
major: 1,
184+
minor: 0,
185+
patch: 0,
186+
prerelease: [],
187+
build: [],
188+
}]];
189+
// FIXME(kt3k): This demonstrates a bug. This should be false
190+
assertEquals(lessThanRange(version, range), true);
191+
});

semver/parse_range.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -141,27 +141,25 @@ function handleRightHyphenRangeGroups(
141141
major: +rightGroups.major,
142142
minor: +rightGroups.minor,
143143
patch: +rightGroups.patch,
144-
prerelease: rightGroups.prerelease
145-
? parsePrerelease(rightGroups.prerelease)
146-
: [],
144+
prerelease: [],
147145
build: [],
148146
};
149147
}
150-
function parseHyphenRange(range: string): Comparator[] | undefined {
148+
function parseHyphenRange(range: string): Comparator[] | null {
151149
const leftMatch = range.match(new RegExp(`^${XRANGE}`));
152150
const leftGroup = leftMatch?.groups;
153-
if (!leftGroup) return;
151+
if (!leftGroup) return null;
154152
const leftLength = leftMatch[0].length;
155153

156154
const hyphenMatch = range.slice(leftLength).match(/^\s+-\s+/);
157-
if (!hyphenMatch) return;
155+
if (!hyphenMatch) return null;
158156
const hyphenLength = hyphenMatch[0].length;
159157

160158
const rightMatch = range.slice(leftLength + hyphenLength).match(
161159
new RegExp(`^${XRANGE}\\s*$`),
162160
);
163161
const rightGroups = rightMatch?.groups;
164-
if (!rightGroups) return;
162+
if (!rightGroups) return null;
165163

166164
const from = handleLeftHyphenRangeGroups(leftGroup as RangeRegExpGroups);
167165
const to = handleRightHyphenRangeGroups(rightGroups as RangeRegExpGroups);
@@ -255,7 +253,7 @@ function handleLessThanOperator(groups: RangeRegExpGroups): Comparator[] {
255253
if (majorIsWildcard) return [{ operator: "<", major: 0, minor: 0, patch: 0 }];
256254
if (minorIsWildcard) {
257255
if (patchIsWildcard) return [{ operator: "<", major, minor: 0, patch: 0 }];
258-
return [{ operator: "<", major, minor, patch: 0 }];
256+
return [{ operator: "<", major, minor: 0, patch: 0 }];
259257
}
260258
if (patchIsWildcard) return [{ operator: "<", major, minor, patch: 0 }];
261259
const prerelease = parsePrerelease(groups.prerelease ?? "");
@@ -318,7 +316,7 @@ function handleGreaterOrEqualOperator(groups: RangeRegExpGroups): Comparator[] {
318316
if (majorIsWildcard) return [ALL];
319317
if (minorIsWildcard) {
320318
if (patchIsWildcard) return [{ operator: ">=", major, minor: 0, patch: 0 }];
321-
return [{ operator: ">=", major, minor, patch: 0 }];
319+
return [{ operator: ">=", major, minor: 0, patch: 0 }];
322320
}
323321
if (patchIsWildcard) return [{ operator: ">=", major, minor, patch: 0 }];
324322
const prerelease = parsePrerelease(groups.prerelease ?? "");

semver/parse_range_test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,3 +666,24 @@ Deno.test("parseRange() throws on invalid range", () => {
666666
'Cannot parse version range: range "blerg" is invalid',
667667
);
668668
});
669+
670+
Deno.test("parseRange() handles wildcards", () => {
671+
assertEquals(parseRange("<1.*"), [
672+
[{ operator: "<", major: 1, minor: 0, patch: 0 }],
673+
]);
674+
assertEquals(parseRange("<1.*.0"), [
675+
[{ operator: "<", major: 1, minor: 0, patch: 0 }],
676+
]);
677+
assertEquals(parseRange("<1.*.*"), [
678+
[{ operator: "<", major: 1, minor: 0, patch: 0 }],
679+
]);
680+
assertEquals(parseRange(">=1.*.0"), [
681+
[{ operator: ">=", major: 1, minor: 0, patch: 0 }],
682+
]);
683+
assertEquals(parseRange(">=1.*.*"), [
684+
[{ operator: ">=", major: 1, minor: 0, patch: 0 }],
685+
]);
686+
assertEquals(parseRange(">=1.0.*"), [
687+
[{ operator: ">=", major: 1, minor: 0, patch: 0 }],
688+
]);
689+
});

semver/try_parse_range_test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
3+
import { assertEquals } from "@std/assert";
4+
import { tryParseRange } from "./try_parse_range.ts";
5+
import type { Range } from "./types.ts";
6+
7+
Deno.test("tryParseRange()", () => {
8+
const actual = tryParseRange(">=1.2.3 <1.2.4");
9+
const expected: Range = [
10+
[
11+
{
12+
operator: ">=",
13+
major: 1,
14+
minor: 2,
15+
patch: 3,
16+
prerelease: [],
17+
build: [],
18+
},
19+
{
20+
operator: "<",
21+
major: 1,
22+
minor: 2,
23+
patch: 4,
24+
prerelease: [],
25+
build: [],
26+
},
27+
],
28+
];
29+
assertEquals(actual, expected);
30+
});
31+
32+
Deno.test("tryParseRange() handles invalid range", () => {
33+
assertEquals(tryParseRange("blerg"), undefined);
34+
});

semver/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type ReleaseType =
2121
export type Operator =
2222
| undefined
2323
| "="
24+
// Note: `!=` operator type does not exist in npm:semver
2425
| "!="
2526
| ">"
2627
| ">="

0 commit comments

Comments
 (0)