From 79e9937d4535283d2f6c695e0b4610a0dc36c00a Mon Sep 17 00:00:00 2001 From: Dan Dascalescu Date: Mon, 9 Dec 2024 14:09:22 +0800 Subject: [PATCH] Stricter typing for assertLess, assertLessOrEqual, asserGreater, assertGreaterOrEqual - disallow undefined actual values because any comparison with undefined returns false - disallow null expected values - narrow the returned type to exclude undefined --- assert/greater.ts | 9 +++++++-- assert/greater_or_equal.ts | 9 +++++---- assert/greater_or_equal_test.ts | 30 +++++++++++++++++++++++++++++ assert/greater_test.ts | 29 ++++++++++++++++++++++++++++ assert/less.ts | 9 +++++++-- assert/less_or_equal.ts | 9 +++++---- assert/less_or_equal_test.ts | 34 +++++++++++++++++++++++++++++++-- assert/less_test.ts | 29 ++++++++++++++++++++++++++++ 8 files changed, 144 insertions(+), 14 deletions(-) diff --git a/assert/greater.ts b/assert/greater.ts index 41738156a1175..965b2b5f92812 100644 --- a/assert/greater.ts +++ b/assert/greater.ts @@ -21,8 +21,13 @@ import { AssertionError } from "./assertion_error.ts"; * @param expected The expected value to compare. * @param msg The optional message to display if the assertion fails. */ -export function assertGreater(actual: T, expected: T, msg?: string) { - if (actual > expected) return; +export function assertGreater( + actual: Exclude | null, + expected: NonNullable, + msg?: string, +): asserts actual is Exclude | null { + // Coerce null to 0 to avoid "Object is possibly null" + if ((actual ?? 0) > expected) return; const actualString = format(actual); const expectedString = format(expected); diff --git a/assert/greater_or_equal.ts b/assert/greater_or_equal.ts index 65fc338fe9c6a..1d454f885b86e 100644 --- a/assert/greater_or_equal.ts +++ b/assert/greater_or_equal.ts @@ -22,11 +22,12 @@ import { AssertionError } from "./assertion_error.ts"; * @param msg The optional message to display if the assertion fails. */ export function assertGreaterOrEqual( - actual: T, - expected: T, + actual: Exclude | null, + expected: NonNullable, msg?: string, -) { - if (actual >= expected) return; +): asserts actual is Exclude | null { + // Coerce null to 0 to avoid "Object is possibly null" + if ((actual ?? 0) >= expected) return; const actualString = format(actual); const expectedString = format(expected); diff --git a/assert/greater_or_equal_test.ts b/assert/greater_or_equal_test.ts index 2a7040fc0f800..507e7dc07fdb0 100644 --- a/assert/greater_or_equal_test.ts +++ b/assert/greater_or_equal_test.ts @@ -1,11 +1,41 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { assertGreaterOrEqual, assertThrows } from "./mod.ts"; +import { assertType, type IsExact } from "../testing/types.ts"; Deno.test("assertGreaterOrEqual() matches when actual value is greater or equal than expected value", () => { assertGreaterOrEqual(2, 1); assertGreaterOrEqual(1n, 1n); + assertGreaterOrEqual(1.1, 1); + assertGreaterOrEqual(null, 0); // coerced to 0 }); Deno.test("assertGreaterOrEqual() throws when actual value is smaller than expected value", () => { assertThrows(() => assertGreaterOrEqual(1, 2)); + assertThrows(() => assertGreaterOrEqual(null, 1)); + + // Compile-time errors + // assertThrows(() => assertGreater(undefined, 1)); + // assertThrows(() => assertGreater(0, null)); +}); + +Deno.test("assertGreaterOrEqual() on strings", () => { + // Strings + assertGreaterOrEqual("", ""); + assertThrows(() => assertGreaterOrEqual("", "a")); + assertThrows(() => assertGreaterOrEqual(null, "a")); +}); + +Deno.test("assertGreater type narrowing", () => { + const n = 0 as number | undefined; + // @ts-expect-error -- `undefined` not allowed for n; disable to see compile-time error below + assertGreaterOrEqual(n, 0); // `undefined` narrowed out + assertType>(true); + const s = "" as string | undefined; + // @ts-expect-error -- `undefined` not allowed for s + assertGreaterOrEqual(s, ""); // `undefined` narrowed out + assertType>(true); + const b = false as boolean | undefined; + // @ts-expect-error -- `undefined` not allowed for b + assertGreaterOrEqual(b, false); // `undefined` narrowed out + assertType>(true); }); diff --git a/assert/greater_test.ts b/assert/greater_test.ts index f8350ef33958d..e4fdd3a9ab79d 100644 --- a/assert/greater_test.ts +++ b/assert/greater_test.ts @@ -1,12 +1,41 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { assertGreater, assertThrows } from "./mod.ts"; +import { assertType, type IsExact } from "../testing/types.ts"; Deno.test("assertGreaterOrEqual() matches when actual value is greater than expected value", () => { assertGreater(2, 1); assertGreater(2n, 1n); assertGreater(1.1, 1); + assertGreater(null, -1); // coerced to 0 }); Deno.test("assertGreaterOrEqual() throws when actual value is smaller or equal than expected value", () => { assertThrows(() => assertGreater(1, 2)); + assertThrows(() => assertGreater(null, 0)); + + // Compile-time errors + // assertThrows(() => assertGreater(undefined, 1)); + // assertThrows(() => assertGreater(0, null)); +}); + +Deno.test("assertGreater() on strings", () => { + // Strings + assertGreater("b", "a"); + assertThrows(() => assertGreater("", "a")); + assertThrows(() => assertGreater(null, "a")); +}); + +Deno.test("assertGreater type narrowing", () => { + const n = 0 as number | undefined; + // @ts-expect-error -- `undefined` not allowed for n; disable to see compile-time error below + assertGreater(n, -1); // `undefined` narrowed out + assertType>(true); + const s = "a" as string | undefined; + // @ts-expect-error -- `undefined` not allowed for s + assertGreater(s, ""); // `undefined` narrowed out + assertType>(true); + const b = true as boolean | undefined; + // @ts-expect-error -- `undefined` not allowed for b + assertGreater(b, false); // `undefined` narrowed out + assertType>(true); }); diff --git a/assert/less.ts b/assert/less.ts index 2d952cf7087e7..1e5c71ed51356 100644 --- a/assert/less.ts +++ b/assert/less.ts @@ -20,8 +20,13 @@ import { AssertionError } from "./assertion_error.ts"; * @param expected The expected value to compare. * @param msg The optional message to display if the assertion fails. */ -export function assertLess(actual: T, expected: T, msg?: string) { - if (actual < expected) return; +export function assertLess( + actual: Exclude | null, + expected: NonNullable, + msg?: string, +): asserts actual is Exclude | null { + // Coerce null to 0 to avoid "Object is possibly null" + if ((actual ?? 0) < expected) return; const actualString = format(actual); const expectedString = format(expected); diff --git a/assert/less_or_equal.ts b/assert/less_or_equal.ts index 308e32498a2fd..e1496fd1663e3 100644 --- a/assert/less_or_equal.ts +++ b/assert/less_or_equal.ts @@ -22,11 +22,12 @@ import { AssertionError } from "./assertion_error.ts"; * @param msg The optional message to display if the assertion fails. */ export function assertLessOrEqual( - actual: T, - expected: T, + actual: Exclude | null, + expected: NonNullable, msg?: string, -) { - if (actual <= expected) return; +): asserts actual is Exclude | null { + // Coerce null to 0 to avoid "Object is possibly null" + if ((actual ?? 0) <= expected) return; const actualString = format(actual); const expectedString = format(expected); diff --git a/assert/less_or_equal_test.ts b/assert/less_or_equal_test.ts index c69b42ca1676c..4c716fb6158cf 100644 --- a/assert/less_or_equal_test.ts +++ b/assert/less_or_equal_test.ts @@ -1,9 +1,39 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { assertLessOrEqual, assertThrows } from "./mod.ts"; +import { assertType, type IsExact } from "../testing/types.ts"; -Deno.test("assertLessOrEqual", () => { +Deno.test("assertLessOrEqualOrEqual", () => { + // Numbers assertLessOrEqual(1, 2); - assertLessOrEqual(1n, 1n); + assertLessOrEqual(1n, 2n); + assertLessOrEqual(1, 1.1); + assertLessOrEqual(null, 1); // coerced to 0 + // Failures assertThrows(() => assertLessOrEqual(2, 1)); + assertThrows(() => assertLessOrEqual(null, -1)); + + // Compile-time errors + // assertThrows(() => assertLessOrEqual(undefined, 1)); + // assertThrows(() => assertLessOrEqual(0, null)); + + // Strings + assertLessOrEqual("a", "a"); + assertThrows(() => assertLessOrEqual("a", "")); + assertThrows(() => assertLessOrEqual(null, "a")); +}); + +Deno.test("assertLessOrEqualOrEqual() type narrowing", () => { + const n = 0 as number | undefined; + // @ts-expect-error -- `undefined` not allowed for n + assertLessOrEqual(n, 0); // `undefined` narrowed out + assertType>(true); + const s = "" as string | undefined; + // @ts-expect-error -- `undefined` not allowed for s + assertLessOrEqual(s, ""); // `undefined` narrowed out + assertType>(true); + const b = false as boolean | undefined; + // @ts-expect-error -- `undefined` not allowed for b + assertLessOrEqual(b, false); // `undefined` narrowed out + assertType>(true); }); diff --git a/assert/less_test.ts b/assert/less_test.ts index 48b79a22c3f63..cf78a1fc3a2d4 100644 --- a/assert/less_test.ts +++ b/assert/less_test.ts @@ -1,10 +1,39 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { assertLess, assertThrows } from "./mod.ts"; +import { assertType, type IsExact } from "../testing/types.ts"; Deno.test("assertLess", () => { + // Numbers assertLess(1, 2); assertLess(1n, 2n); assertLess(1, 1.1); + assertLess(null, 1); // coerced to 0 + // Failures assertThrows(() => assertLess(2, 1)); + assertThrows(() => assertLess(null, -1)); + + // Compile-time errors + // assertThrows(() => assertLess(undefined, 1)); + // assertThrows(() => assertLess(-1, null)); + + // Strings + assertLess("a", "b"); + assertThrows(() => assertLess("a", "")); + assertThrows(() => assertLess(null, "a")); +}); + +Deno.test("assertLess() type narrowing", () => { + const n = 0 as number | undefined; + // @ts-expect-error -- `undefined` not allowed for n; disable to see compile-time error below + assertLess(n, 1); // `undefined` narrowed out + assertType>(true); + const s = "" as string | undefined; + // @ts-expect-error -- `undefined` not allowed for s + assertLess(s, "a"); // `undefined` narrowed out + assertType>(true); + const b = false as boolean | undefined; + // @ts-expect-error -- `undefined` not allowed for b + assertLess(b, true); // `undefined` narrowed out + assertType>(true); });