Skip to content

Commit f3e236c

Browse files
committed
feat(semver): add input validation to parseVersion and compareVersions
Signed-off-by: leocavalcante <leo@cavalcante.dev>
1 parent 1a584f6 commit f3e236c

File tree

3 files changed

+153
-0
lines changed

3 files changed

+153
-0
lines changed

src/semver.d.mts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ export interface ParsedVersion {
1616
*
1717
* @param version - The version string (e.g., "1.2.3")
1818
* @returns Parsed version object or null if invalid
19+
* @throws {TypeError} If version is not a string
1920
*
2021
* @example
2122
* parseVersion("1.2.3") // { major: 1, minor: 2, patch: 3 }
2223
* parseVersion("invalid") // null
24+
* parseVersion(null) // throws TypeError
2325
*/
2426
export function parseVersion(version: string): ParsedVersion | null
2527

@@ -29,11 +31,13 @@ export function parseVersion(version: string): ParsedVersion | null
2931
* @param a - First version
3032
* @param b - Second version
3133
* @returns -1 if a < b, 0 if a == b, 1 if a > b
34+
* @throws {TypeError} If a or b is not a valid ParsedVersion object with numeric major, minor, and patch properties
3235
*
3336
* @example
3437
* compareVersions({ major: 1, minor: 0, patch: 0 }, { major: 2, minor: 0, patch: 0 }) // -1
3538
* compareVersions({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 0 }) // 0
3639
* compareVersions({ major: 2, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 0 }) // 1
40+
* compareVersions({}, {}) // throws TypeError
3741
*/
3842
export function compareVersions(a: ParsedVersion, b: ParsedVersion): -1 | 0 | 1
3943

src/semver.mjs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@
1010
*
1111
* @param {string} version - The version string (e.g., "1.2.3")
1212
* @returns {{ major: number, minor: number, patch: number } | null} Parsed version or null if invalid
13+
* @throws {TypeError} If version is not a string
1314
*/
1415
export function parseVersion(version) {
16+
if (typeof version !== "string") {
17+
throw new TypeError(
18+
`parseVersion: version must be a string, got ${version === null ? "null" : typeof version}`,
19+
)
20+
}
1521
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/)
1622
if (!match) return null
1723
return {
@@ -21,14 +27,43 @@ export function parseVersion(version) {
2127
}
2228
}
2329

30+
/**
31+
* Validates that a value is a valid ParsedVersion object.
32+
*
33+
* @param {unknown} value - The value to validate
34+
* @param {string} paramName - The parameter name for error messages
35+
* @throws {TypeError} If value is not a valid ParsedVersion object
36+
*/
37+
function validateParsedVersion(value, paramName) {
38+
if (value === null || value === undefined) {
39+
throw new TypeError(
40+
`compareVersions: ${paramName} must be a ParsedVersion object, got ${value === null ? "null" : "undefined"}`,
41+
)
42+
}
43+
if (typeof value !== "object") {
44+
throw new TypeError(
45+
`compareVersions: ${paramName} must be a ParsedVersion object, got ${typeof value}`,
46+
)
47+
}
48+
const v = /** @type {Record<string, unknown>} */ (value)
49+
if (typeof v.major !== "number" || typeof v.minor !== "number" || typeof v.patch !== "number") {
50+
throw new TypeError(
51+
`compareVersions: ${paramName} must have numeric major, minor, and patch properties`,
52+
)
53+
}
54+
}
55+
2456
/**
2557
* Compares two parsed version objects.
2658
*
2759
* @param {{ major: number, minor: number, patch: number }} a - First version
2860
* @param {{ major: number, minor: number, patch: number }} b - Second version
2961
* @returns {number} -1 if a < b, 0 if a == b, 1 if a > b
62+
* @throws {TypeError} If a or b is not a valid ParsedVersion object
3063
*/
3164
export function compareVersions(a, b) {
65+
validateParsedVersion(a, "a")
66+
validateParsedVersion(b, "b")
3267
if (a.major !== b.major) return a.major < b.major ? -1 : 1
3368
if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1
3469
if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1

tests/semver.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,31 @@ describe("semver.mjs exports", () => {
2424
expect(parseVersion("0.0.1")).toEqual({ major: 0, minor: 0, patch: 1 })
2525
expect(parseVersion("999.999.999")).toEqual({ major: 999, minor: 999, patch: 999 })
2626
})
27+
28+
it("should throw TypeError for null", () => {
29+
expect(() => parseVersion(null as unknown as string)).toThrow(TypeError)
30+
expect(() => parseVersion(null as unknown as string)).toThrow(
31+
"parseVersion: version must be a string, got null",
32+
)
33+
})
34+
35+
it("should throw TypeError for undefined", () => {
36+
expect(() => parseVersion(undefined as unknown as string)).toThrow(TypeError)
37+
expect(() => parseVersion(undefined as unknown as string)).toThrow(
38+
"parseVersion: version must be a string, got undefined",
39+
)
40+
})
41+
42+
it("should throw TypeError for non-string inputs", () => {
43+
expect(() => parseVersion(123 as unknown as string)).toThrow(TypeError)
44+
expect(() => parseVersion(123 as unknown as string)).toThrow(
45+
"parseVersion: version must be a string, got number",
46+
)
47+
expect(() => parseVersion({} as unknown as string)).toThrow(TypeError)
48+
expect(() => parseVersion({} as unknown as string)).toThrow(
49+
"parseVersion: version must be a string, got object",
50+
)
51+
})
2752
})
2853

2954
describe("compareVersions", () => {
@@ -76,6 +101,95 @@ describe("semver.mjs exports", () => {
76101
compareVersions({ major: 1, minor: 2, patch: 0 }, { major: 1, minor: 1, patch: 9 }),
77102
).toBe(1)
78103
})
104+
105+
it("should throw TypeError for null parameter a", () => {
106+
expect(() =>
107+
compareVersions(null as unknown as { major: number; minor: number; patch: number }, {
108+
major: 1,
109+
minor: 0,
110+
patch: 0,
111+
}),
112+
).toThrow(TypeError)
113+
expect(() =>
114+
compareVersions(null as unknown as { major: number; minor: number; patch: number }, {
115+
major: 1,
116+
minor: 0,
117+
patch: 0,
118+
}),
119+
).toThrow("compareVersions: a must be a ParsedVersion object, got null")
120+
})
121+
122+
it("should throw TypeError for undefined parameter b", () => {
123+
expect(() =>
124+
compareVersions(
125+
{ major: 1, minor: 0, patch: 0 },
126+
undefined as unknown as { major: number; minor: number; patch: number },
127+
),
128+
).toThrow(TypeError)
129+
expect(() =>
130+
compareVersions(
131+
{ major: 1, minor: 0, patch: 0 },
132+
undefined as unknown as { major: number; minor: number; patch: number },
133+
),
134+
).toThrow("compareVersions: b must be a ParsedVersion object, got undefined")
135+
})
136+
137+
it("should throw TypeError for non-object parameters", () => {
138+
expect(() =>
139+
compareVersions("1.0.0" as unknown as { major: number; minor: number; patch: number }, {
140+
major: 1,
141+
minor: 0,
142+
patch: 0,
143+
}),
144+
).toThrow(TypeError)
145+
expect(() =>
146+
compareVersions("1.0.0" as unknown as { major: number; minor: number; patch: number }, {
147+
major: 1,
148+
minor: 0,
149+
patch: 0,
150+
}),
151+
).toThrow("compareVersions: a must be a ParsedVersion object, got string")
152+
})
153+
154+
it("should throw TypeError for empty objects", () => {
155+
expect(() =>
156+
compareVersions({} as unknown as { major: number; minor: number; patch: number }, {
157+
major: 1,
158+
minor: 0,
159+
patch: 0,
160+
}),
161+
).toThrow(TypeError)
162+
expect(() =>
163+
compareVersions({} as unknown as { major: number; minor: number; patch: number }, {
164+
major: 1,
165+
minor: 0,
166+
patch: 0,
167+
}),
168+
).toThrow("compareVersions: a must have numeric major, minor, and patch properties")
169+
})
170+
171+
it("should throw TypeError for objects with non-numeric properties", () => {
172+
expect(() =>
173+
compareVersions(
174+
{ major: "1", minor: 0, patch: 0 } as unknown as {
175+
major: number
176+
minor: number
177+
patch: number
178+
},
179+
{ major: 1, minor: 0, patch: 0 },
180+
),
181+
).toThrow(TypeError)
182+
expect(() =>
183+
compareVersions(
184+
{ major: 1, minor: null, patch: 0 } as unknown as {
185+
major: number
186+
minor: number
187+
patch: number
188+
},
189+
{ major: 1, minor: 0, patch: 0 },
190+
),
191+
).toThrow("compareVersions: a must have numeric major, minor, and patch properties")
192+
})
79193
})
80194

81195
describe("checkVersionCompatibility", () => {

0 commit comments

Comments
 (0)