Repository: denoland/deno
Pull request: fix: validate integer values in `Deno.exitCode` setter

<!--
Before submitting a PR, please read https://docs.deno.com/runtime/manual/references/contributing

1. Give the PR a descriptive title.

  Examples of good title:
    - fix(std/http): Fix race condition in server
    - docs(console): Update docstrings
    - feat(doc): Handle nested reexports

  Examples of bad title:
    - fix #7123
    - update docs
    - fix bugs

2. Ensure there is a related issue and it is referenced in the PR text.
3. Ensure there are tests that cover the changes.
4. Ensure `cargo test` passes.
5. Ensure `./tools/format.js` passes without changing files.
6. Ensure `./tools/lint.js` passes.
7. Open as a draft PR if your work is still in progress. The CI won't run
   all steps, but you can add '[ci]' to a commit message to force it to.
8. If you would like to run the benchmarks on the CI, add the 'ci-bench' label.
-->

related: #23609

This PR includes the following:

* Delete the non-executed `cli/tests/unit/os_test.ts` and move its tests to `tests/unit/os_test.ts`
* Fix to validate integer values in `Deno.exitCode` setter (no implicit type conversions)
* Fix to match `process.exitCode` to Node.js behavior (my env: v22.2.0, M1 Mac)

Changed files:
filename: cli/tests/unit/os_test.ts
patch: @@ -1,94 +0,0 @@
-// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
-
-import { assertEquals, assertThrows } from "../../testing/asserts.ts";
-
-Deno.test("Deno.exitCode getter and setter", () => {
-  // Initial value is 0
-  assertEquals(Deno.exitCode, 0);
-
-  // Set a new value
-  Deno.exitCode = 5;
-  assertEquals(Deno.exitCode, 5);
-
-  // Reset to initial value
-  Deno.exitCode = 0;
-  assertEquals(Deno.exitCode, 0);
-});
-
-Deno.test("Setting Deno.exitCode to NaN throws TypeError", () => {
-  // @ts-expect-error;
-  Deno.exitCode = "123";
-  assertEquals(Deno.exitCode, 123);
-
-  // Reset
-  Deno.exitCode = 0;
-  assertEquals(Deno.exitCode, 0);
-
-  // Throws on non-number values
-  assertThrows(
-    () => {
-      // @ts-expect-error Testing for runtime error
-      Deno.exitCode = "not a number";
-    },
-    TypeError,
-    "Exit code must be a number.",
-  );
-});
-
-Deno.test("Setting Deno.exitCode does not cause an immediate exit", () => {
-  let exited = false;
-  const originalExit = Deno.exit;
-
-  // @ts-expect-error; read-only
-  Deno.exit = () => {
-    exited = true;
-  };
-
-  Deno.exitCode = 1;
-  assertEquals(exited, false);
-
-  // @ts-expect-error; read-only
-  Deno.exit = originalExit;
-});
-
-Deno.test("Running Deno.exit(value) overrides Deno.exitCode", () => {
-  let args: unknown[] | undefined;
-
-  const originalExit = Deno.exit;
-  // @ts-expect-error; read-only
-  Deno.exit = (...x) => {
-    args = x;
-  };
-
-  Deno.exitCode = 42;
-  Deno.exit(0);
-
-  assertEquals(args, [0]);
-  // @ts-expect-error; read-only
-  Deno.exit = originalExit;
-});
-
-Deno.test("Running Deno.exit() uses Deno.exitCode as fallback", () => {
-  let args: unknown[] | undefined;
-
-  const originalExit = Deno.exit;
-  // @ts-expect-error; read-only
-  Deno.exit = (...x) => {
-    args = x;
-  };
-
-  Deno.exitCode = 42;
-  Deno.exit();
-
-  assertEquals(args, [42]);
-  // @ts-expect-error; read-only
-  Deno.exit = originalExit;
-});
-
-Deno.test("Retrieving the set exit code before process termination", () => {
-  Deno.exitCode = 42;
-  assertEquals(Deno.exitCode, 42);
-
-  // Reset to initial value
-  Deno.exitCode = 0;
-});

filename: ext/node/polyfills/process.ts
patch: @@ -19,6 +19,7 @@ import { report } from "ext:deno_node/internal/process/report.ts";
 import { validateString } from "ext:deno_node/internal/validators.mjs";
 import {
   ERR_INVALID_ARG_TYPE,
+  ERR_OUT_OF_RANGE,
   ERR_UNKNOWN_SIGNAL,
   errnoException,
 } from "ext:deno_node/internal/errors.ts";
@@ -439,38 +440,22 @@ Object.defineProperty(Process.prototype, "exitCode", {
     return ProcessExitCode;
   },
   set(code: number | string | null | undefined) {
-    let parsedCode;
-
-    if (typeof code === "number") {
-      if (Number.isNaN(code)) {
-        parsedCode = 1;
-        denoOs.setExitCode(parsedCode);
-        ProcessExitCode = parsedCode;
-        return;
-      }
-
-      // This is looser than `denoOs.setExitCode` which requires exit code
-      // to be decimal or string of a decimal, but Node accept eg. 0x10.
-      parsedCode = parseInt(code);
-      denoOs.setExitCode(parsedCode);
-      ProcessExitCode = parsedCode;
-      return;
+    let parsedCode: number;
+    if (code == null) {
+      parsedCode = 0;
+    } else if (typeof code === "number") {
+      parsedCode = code;
+    } else if (typeof code === "string") {
+      parsedCode = Number(code);
+    } else {
+      throw new ERR_INVALID_ARG_TYPE("code", "number", code);
     }
 
-    if (typeof code === "string") {
-      parsedCode = parseInt(code);
-      if (Number.isNaN(parsedCode)) {
-        throw new TypeError(
-          `The "code" argument must be of type number. Received type ${typeof code} (${code})`,
-        );
-      }
-      denoOs.setExitCode(parsedCode);
-      ProcessExitCode = parsedCode;
-      return;
+    if (!Number.isInteger(parsedCode)) {
+      throw new ERR_OUT_OF_RANGE("code", "an integer", parsedCode);
     }
 
-    // TODO(bartlomieju): hope for the best here. This should be further tightened.
-    denoOs.setExitCode(code);
+    denoOs.setExitCode(parsedCode);
     ProcessExitCode = code;
   },
 });

filename: runtime/js/30_os.js
patch: @@ -22,7 +22,8 @@ import {
 const {
   Error,
   FunctionPrototypeBind,
-  NumberParseInt,
+  NumberIsInteger,
+  RangeError,
   SymbolFor,
   TypeError,
 } = primordials;
@@ -102,13 +103,17 @@ function getExitCode() {
 }
 
 function setExitCode(value) {
-  const code = NumberParseInt(value, 10);
-  if (typeof code !== "number") {
+  if (typeof value !== "number") {
     throw new TypeError(
-      `Exit code must be a number, got: ${code} (${typeof code}).`,
+      `Exit code must be a number, got: ${value} (${typeof value})`,
     );
   }
-  op_set_exit_code(code);
+  if (!NumberIsInteger(value)) {
+    throw new RangeError(
+      `Exit code must be an integer, got: ${value}`,
+    );
+  }
+  op_set_exit_code(value);
 }
 
 function setEnv(key, value) {

filename: tests/unit/os_test.ts
patch: @@ -302,3 +302,70 @@ Deno.test(function memoryUsage() {
   assert(typeof mem.external === "number");
   assert(mem.rss >= mem.heapTotal);
 });
+
+Deno.test("Deno.exitCode getter and setter", () => {
+  // Initial value is 0
+  assertEquals(Deno.exitCode, 0);
+
+  try {
+    // Set a new value
+    Deno.exitCode = 5;
+    assertEquals(Deno.exitCode, 5);
+  } finally {
+    // Reset to initial value
+    Deno.exitCode = 0;
+  }
+
+  assertEquals(Deno.exitCode, 0);
+});
+
+Deno.test("Setting Deno.exitCode to non-number throws TypeError", () => {
+  // Throws on non-number values
+  assertThrows(
+    () => {
+      // @ts-expect-error Testing for runtime error
+      Deno.exitCode = "123";
+    },
+    TypeError,
+    "Exit code must be a number, got: 123 (string)",
+  );
+
+  // Throws on bigint values
+  assertThrows(
+    () => {
+      // @ts-expect-error Testing for runtime error
+      Deno.exitCode = 1n;
+    },
+    TypeError,
+    "Exit code must be a number, got: 1 (bigint)",
+  );
+});
+
+Deno.test("Setting Deno.exitCode to non-integer throws RangeError", () => {
+  // Throws on non-integer values
+  assertThrows(
+    () => {
+      Deno.exitCode = 3.14;
+    },
+    RangeError,
+    "Exit code must be an integer, got: 3.14",
+  );
+});
+
+Deno.test("Setting Deno.exitCode does not cause an immediate exit", () => {
+  let exited = false;
+
+  const originalExit = Deno.exit;
+  // @ts-expect-error; read-only
+  Deno.exit = () => {
+    exited = true;
+  };
+
+  try {
+    Deno.exitCode = 1;
+    assertEquals(exited, false);
+  } finally {
+    Deno.exit = originalExit;
+    Deno.exitCode = 0;
+  }
+});

filename: tests/unit_node/process_test.ts
patch: @@ -813,10 +813,6 @@ Deno.test("process.exitCode in should change exit code", async () => {
     "import process from 'node:process'; process.exitCode = 127;",
     127,
   );
-  await exitCodeTest(
-    "import process from 'node:process'; process.exitCode = 2.5;",
-    2,
-  );
   await exitCodeTest(
     "import process from 'node:process'; process.exitCode = '10';",
     10,
@@ -825,10 +821,6 @@ Deno.test("process.exitCode in should change exit code", async () => {
     "import process from 'node:process'; process.exitCode = '0x10';",
     16,
   );
-  await exitCodeTest(
-    "import process from 'node:process'; process.exitCode = NaN;",
-    1,
-  );
 });
 
 Deno.test("Deno.exit should override process exit", async () => {