From 971d85fc81926c4703724bbb40500aefc43f2073 Mon Sep 17 00:00:00 2001 From: Rym Ameuri Date: Fri, 4 Sep 2026 20:18:15 +0000 Subject: [PATCH 1/2] refactor(codemode): reduce complexity in coerceToString and invokeCoercion Replace instanceof/name if-chains with lookup tables to cut function complexity and return-path count flagged by qlty. --- packages/codemode/src/stdlib/value.ts | 75 +++++++++++++++++---------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/packages/codemode/src/stdlib/value.ts b/packages/codemode/src/stdlib/value.ts index ab40dc07..6739e18c 100644 --- a/packages/codemode/src/stdlib/value.ts +++ b/packages/codemode/src/stdlib/value.ts @@ -27,21 +27,34 @@ export const errorBrandName = (value: unknown): string | undefined => export const boundedData = (value: unknown, label: string): unknown => copyIn(value, label, true) +type SandboxStringFormatter = readonly [ + ctor: new (...args: never[]) => unknown, + format: (value: never) => string, +] + +const sandboxStringFormatters: readonly SandboxStringFormatter[] = [ + [SandboxDate, (value: SandboxDate) => (Number.isFinite(value.time) ? new Date(value.time).toISOString() : "Invalid Date")], + [SandboxRegExp, (value: SandboxRegExp) => `/${value.regex.source}/${value.regex.flags}`], + [SandboxMap, () => "[object Map]"], + [SandboxSet, () => "[object Set]"], + [SandboxURL, (value: SandboxURL) => value.url.href], + [SandboxURLSearchParams, (value: SandboxURLSearchParams) => value.params.toString()], +] + +const coerceArrayToString = (value: unknown[]): string => + value.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",") + export const coerceToString = (value: unknown): string => { if (value === null) return "null" if (value === undefined) return "undefined" - if (value instanceof SandboxDate) - return Number.isFinite(value.time) ? new Date(value.time).toISOString() : "Invalid Date" - if (value instanceof SandboxRegExp) return `/${value.regex.source}/${value.regex.flags}` - if (value instanceof SandboxMap) return "[object Map]" - if (value instanceof SandboxSet) return "[object Set]" - if (value instanceof SandboxURL) return value.url.href - if (value instanceof SandboxURLSearchParams) return value.params.toString() + + const formatter = sandboxStringFormatters.find(([ctor]) => value instanceof ctor) + if (formatter) return formatter[1](value as never) + if (typeof value === "object") { - return Array.isArray(value) - ? value.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",") - : "[object Object]" + return Array.isArray(value) ? coerceArrayToString(value) : "[object Object]" } + return String(value) } @@ -51,27 +64,37 @@ export const coerceToNumber = (value: unknown): number => { return value !== null && typeof value === "object" && !Array.isArray(value) ? Number.NaN : Number(value) } +const sandboxCoercions: Record unknown> = { + Number: (raw) => coerceToNumber(raw), + String: (raw) => coerceToString(raw), + parseInt: (raw) => parseInt(coerceToString(raw)), +} + +const valueCoercions: Record unknown> = { + Number: (value) => coerceToNumber(value), + Boolean: (value) => Boolean(value), + parseFloat: (value) => parseFloat(coerceToString(value)), +} + +function coerceParseInt(value: unknown, args: Array, node: AstNode): number { + const radix = args[1] + if (radix !== undefined && typeof radix !== "number") { + throw new InterpreterRuntimeError("parseInt expects a numeric radix.", node) + } + return parseInt(coerceToString(value), radix as number | undefined) +} + export const invokeCoercion = (ref: CoercionFunction, args: Array, node: AstNode): unknown => { const raw = args[0] + if (isSandboxValue(raw)) { if (ref.name === "Boolean") return true - if (ref.name === "Number") return coerceToNumber(raw) - if (ref.name === "String") return coerceToString(raw) - if (ref.name === "parseInt") return parseInt(coerceToString(raw)) - return parseFloat(coerceToString(raw)) + return sandboxCoercions[ref.name]?.(raw) ?? parseFloat(coerceToString(raw)) } - const value = boundedData(args[0], `${ref.name} input`) - if (ref.name === "Number") return coerceToNumber(value) - if (ref.name === "Boolean") return Boolean(value) - if (ref.name === "parseInt") { - const radix = args[1] - if (radix !== undefined && typeof radix !== "number") { - throw new InterpreterRuntimeError("parseInt expects a numeric radix.", node) - } - return parseInt(coerceToString(value), radix) - } - if (ref.name === "parseFloat") return parseFloat(coerceToString(value)) - return coerceToString(value) + + const value = boundedData(raw, `${ref.name} input`) + if (ref.name === "parseInt") return coerceParseInt(value, args, node) + return valueCoercions[ref.name]?.(value) ?? coerceToString(value) } import { type AstNode, CoercionFunction, InterpreterRuntimeError } from "../interpreter/model.js" import { copyIn, type SafeObject } from "../tool-runtime.js" From 732d44e434a76da56702521a5c3d789074d5d483 Mon Sep 17 00:00:00 2001 From: Rym Ameuri Date: Fri, 4 Sep 2026 22:16:26 +0000 Subject: [PATCH 2/2] test(codemode): cover array-to-string coercion and invalid parseInt radix --- packages/codemode/test/stdlib.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/codemode/test/stdlib.test.ts b/packages/codemode/test/stdlib.test.ts index f7831a06..5dbe01e1 100644 --- a/packages/codemode/test/stdlib.test.ts +++ b/packages/codemode/test/stdlib.test.ts @@ -713,3 +713,16 @@ describe("sandbox values at intra-sandbox checkpoints", () => { expect(observed).toStrictEqual([{ when: "1970-01-01T00:00:00.000Z", tags: {} }]) }) }) + +describe("array-to-string coercion", () => { + test("String() on an array joins elements with commas, treating null/undefined as empty", async () => { + expect(await value(`return String([1, null, undefined, 2])`)).toBe("1,,,2") + }) +}) + +describe("parseInt radix validation", () => { + test("rejects a non-numeric radix", async () => { + const err = await error(`return parseInt("10", "x")`) + expect(err.message).toContain("parseInt expects a numeric radix.") + }) +})