Accept mock.fn() without an original

node:test changes, matching Node 24.22:

- mock.fn() and t.mock.fn() without an original return undefined and
  record the arguments of each call, whatever the callers pass. The
  original and the initial implementation are a function that returns
  undefined, so restore() brings that function back.
- mock.fn<F>() with an explicit function type records F's parameters
  when F's return type is void or includes undefined. A return type
  without undefined, such as mock.fn<() => number>(), throws SC2020.

mock.fn()'s default type, (...args: any[]) => undefined, maps to the
checked-dynamic rest ABI used for program-declared (...args: unknown[])
functions: the calls log records the argument array, and the mock's
wrapper spreads it into the implementation.

A function whose fixed parameters are all unknown or any, or that has
none, now converts to a checked-dynamic rest signature without fixed
parameters, such as (...args: unknown[]) => void: each call passes the
arguments in order, undefined for the ones the caller left out. This is
how () => {} reaches mockImplementation and mockImplementationOnce, and
it applies to any such slot in TypeScript programs, which threw SC1090
before.

The mock-fn-default fixture and corpus program 5816 compare with Node,
plain and with SCRIPTC_SAN=1.
This commit is contained in:
Chris Tate
2026-10-19 16:59:05 -05:00
parent 706f473559
commit 0e02fc28ca
17 changed files with 318 additions and 69 deletions
@@ -0,0 +1,39 @@
// A function with only `unknown` parameters, or none, flows into a
// `(...args: unknown[]) => R` slot: each call passes the callers'
// arguments in order, undefined for a parameter the caller left out, and
// drops the extra ones, as JS calls do. Sync and async functions, stored
// in variables and arrays and passed as arguments.
type Handler = (...args: unknown[]) => void;
type AsyncHandler = (...args: unknown[]) => Promise<unknown>;
const none: Handler = () => console.log("none");
const one: Handler = (a: unknown) => console.log("one", a);
const two: Handler = (a: unknown, b: unknown) => console.log("two", a, b);
none();
none(1, 2, 3);
one();
one("x");
one("x", "y");
two(1);
two(1, [2], { three: 3 });
const handlers: Handler[] = [none, one, two];
for (const h of handlers) h("all", true);
function dispatch(h: Handler, ...args: unknown[]): void {
h(...args);
}
dispatch(two, "spread", null);
dispatch(one);
const first: AsyncHandler = async (value: unknown) => value;
const pair: AsyncHandler = async (a: unknown, b: unknown) => [a, b];
async function main(): Promise<void> {
console.log(await first(10, 20));
console.log(await first());
console.log(await pair("p"));
console.log(await pair("p", "q", "r"));
}
main().then(() => console.log("done"));
@@ -30,8 +30,3 @@ console.log(typeof widened);
// `instanceof Promise` on a value that is never a promise.
const record = { v: 1 };
console.log(record instanceof Promise);
// A variadic `unknown[]` slot takes closures with the same variadic ABI.
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
const fixed: HandlerFn = async (first: unknown) => first;
fixed(1, 2).then((v) => console.log(v));
+8 -4
View File
@@ -1,8 +1,9 @@
// node:test mocks lower for mock.fn(original[, implementation]),
// node:test mocks lower for mock.fn([original[, implementation]]),
// mock.method(object, methodName[, implementation]) on records, their
// t.mock twins, and the trackers' restoreAll and reset. The rest of the
// mock surface is refused, each with a named diagnostic: mock.fn() without
// an original; mock.method on a class instance or a builtin object, with
// an original for a function type whose return type does not include
// undefined; mock.method on a class instance or a builtin object, with
// an options object, or with an implementation of another type when the
// result is used; and functions whose union return type has no undefined
// arm (the calls log records undefined for calls that throw).
@@ -11,8 +12,11 @@ import { mock, test } from "node:test";
const add = mock.fn((a: number, b: number) => a + b); // lowers
add(1, 2);
const empty = mock.fn(); // no original function
empty();
const empty = mock.fn(); // lowers
empty(1, "two");
const counted = mock.fn<() => number>(); // no original, returns number
counted();
const obj = { value: () => 1 };
mock.method(obj, "value"); // lowers
+72
View File
@@ -0,0 +1,72 @@
// mock.fn() and t.mock.fn() without an original: the mock returns
// undefined and records the arguments of each call, whatever its callers
// pass. mockImplementation and mockImplementationOnce replace its
// behavior, and restore() brings back the function that returns undefined.
// mock.fn<F>() records F's parameters, for a function type whose return
// type is void or includes undefined.
import assert from "node:assert";
import { mock, test } from "node:test";
test("records calls with any arguments", () => {
const fn = mock.fn();
fn(1, "two");
fn();
fn(true, null, [3]);
assert.strictEqual(fn.mock.callCount(), 3);
const [first, second, third] = fn.mock.calls;
assert.deepStrictEqual(first!.arguments, [1, "two"]);
assert.strictEqual(second!.arguments.length, 0);
assert.deepStrictEqual(third!.arguments, [true, null, [3]]);
assert.ok(first!.result === undefined);
assert.ok(first!.error === undefined);
assert.ok(first!.this === undefined);
});
test("changes implementations", () => {
const seen: string[] = [];
const fn = mock.fn();
fn.mock.mockImplementation((...args: unknown[]) => {
seen.push(`impl ${args.length}`);
return undefined;
});
fn.mock.mockImplementationOnce(() => {
seen.push("once");
return undefined;
});
fn("a");
fn("a", "b");
fn.mock.restore();
fn("c");
assert.deepStrictEqual(seen, ["once", "impl 2"]);
assert.strictEqual(fn.mock.callCount(), 3);
fn.mock.resetCalls();
assert.strictEqual(fn.mock.callCount(), 0);
});
test("records a throwing implementation", () => {
const fn = mock.fn();
fn.mock.mockImplementation(() => {
throw new Error("boom");
});
assert.throws(() => fn(7), { message: "boom" });
const call = fn.mock.calls[0]!;
assert.deepStrictEqual(call.arguments, [7]);
assert.ok(call.error instanceof Error);
});
test("t.mock.fn() and typed mock.fn<F>()", (t) => {
const fn = t.mock.fn();
fn("x");
assert.strictEqual(fn.mock.callCount(), 1);
const typed = t.mock.fn<(a: number, b: string) => void>();
typed(1, "x");
typed(2, "y");
assert.strictEqual(typed.mock.callCount(), 2);
assert.strictEqual(typed.mock.calls[1]!.arguments[0], 2);
assert.strictEqual(typed.mock.calls[1]!.arguments[1], "y");
const maybe = t.mock.fn<() => number | undefined>();
assert.ok(maybe() === undefined);
maybe.mock.mockImplementation(() => 4);
assert.ok(maybe() === 4);
assert.ok(maybe.mock.calls[1]!.result === 4);
});
@@ -55,11 +55,4 @@ maybe-promise-fences.ts:32:13 - error SC1090: 'instanceof Promise' on '{ v: numb
31 | const record = { v: 1 };
32 | console.log(record instanceof Promise);
| ^~~~~~~~~~~~~~~~~~~~~~~~~
33 |
maybe-promise-fences.ts:36:26 - error SC1090: '(unknown) => Promise<unknown>' values where '() => Promise<unknown>' is expected is not supported yet
35 | type HandlerFn = (...args: unknown[]) => Promise<unknown>;
36 | const fixed: HandlerFn = async (first: unknown) => first;
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
37 | fixed(1, 2).then((v) => console.log(v));
33 |
+30 -30
View File
@@ -1,62 +1,62 @@
mock-fences.ts:14:15 - error SC2020: 'mock.fn of this function type' is part of the standard library types but has no scriptc lowering yet
mock-fences.ts:18:17 - error SC2020: 'mock.fn() of a function returning 'number'' is part of the standard library types but has no scriptc lowering yet
13 |
14 | const empty = mock.fn(); // no original function
| ^~~~~~~~~
15 | empty();
17 |
18 | const counted = mock.fn<() => number>(); // no original, returns number
| ^~~~~~~~~~~~~~~~~~~~~~~
19 | counted();
hint: mock functions lower for functions with fixed parameters, or a single rest array parameter, and an implementation of the same type
hint: mock.fn() without an original returns undefined: pass an original, or use a function type whose return type includes undefined
mock-fences.ts:27:13 - error SC2020: 'mock.method on 'Counter'' is part of the standard library types but has no scriptc lowering yet
mock-fences.ts:31:13 - error SC2020: 'mock.method on 'Counter'' is part of the standard library types but has no scriptc lowering yet
26 | }
27 | mock.method(new Counter(), "count"); // class instance
30 | }
31 | mock.method(new Counter(), "count"); // class instance
| ^~~~~~~~~~~~~
28 | mock.method(console, "log"); // builtin object
32 | mock.method(console, "log"); // builtin object
hint: mock.method lowers for objects whose type maps to a record (object literals and interface-typed objects) and a function-valued field named by a string literal
mock-fences.ts:28:13 - error SC2020: 'mock.method on this object' is part of the standard library types but has no scriptc lowering yet
mock-fences.ts:32:13 - error SC2020: 'mock.method on this object' is part of the standard library types but has no scriptc lowering yet
27 | mock.method(new Counter(), "count"); // class instance
28 | mock.method(console, "log"); // builtin object
31 | mock.method(new Counter(), "count"); // class instance
32 | mock.method(console, "log"); // builtin object
| ^~~~~~~
29 | mock.method(obj, "value", { times: 1 }); // options
33 | mock.method(obj, "value", { times: 1 }); // options
hint: mock.method lowers for objects whose type maps to a record (object literals and interface-typed objects) and a function-valued field named by a string literal
mock-fences.ts:29:27 - error SC2020: 'mock.method with an options object' is part of the standard library types but has no scriptc lowering yet
mock-fences.ts:33:27 - error SC2020: 'mock.method with an options object' is part of the standard library types but has no scriptc lowering yet
28 | mock.method(console, "log"); // builtin object
29 | mock.method(obj, "value", { times: 1 }); // options
32 | mock.method(console, "log"); // builtin object
33 | mock.method(obj, "value", { times: 1 }); // options
| ^~~~~~~~~~~~
30 | const other = mock.method(obj, "value", () => "one"); // implementation of another type
34 | const other = mock.method(obj, "value", () => "one"); // implementation of another type
hint: mock options (times, getter and setter) have no lowering
mock-fences.ts:30:41 - error SC2020: 'mock.method with an implementation of another type' is part of the standard library types but has no scriptc lowering yet
mock-fences.ts:34:41 - error SC2020: 'mock.method with an implementation of another type' is part of the standard library types but has no scriptc lowering yet
29 | mock.method(obj, "value", { times: 1 }); // options
30 | const other = mock.method(obj, "value", () => "one"); // implementation of another type
33 | mock.method(obj, "value", { times: 1 }); // options
34 | const other = mock.method(obj, "value", () => "one"); // implementation of another type
| ^~~~~~~~~~~
31 | other();
35 | other();
hint: give the implementation the method's parameter and return types, or discard the result
mock-fences.ts:35:31 - error SC2020: 't.mock.method with an options object' is part of the standard library types but has no scriptc lowering yet
mock-fences.ts:39:31 - error SC2020: 't.mock.method with an options object' is part of the standard library types but has no scriptc lowering yet
34 | t.mock.method(obj, "value"); // lowers
35 | t.mock.method(obj, "value", { times: 2 }); // options
38 | t.mock.method(obj, "value"); // lowers
39 | t.mock.method(obj, "value", { times: 2 }); // options
| ^~~~~~~~~~~~
36 | });
40 | });
hint: mock options (times, getter and setter) have no lowering
mock-fences.ts:38:14 - error SC2020: 'mock.fn of a function returning 'number | string'' is part of the standard library types but has no scriptc lowering yet
mock-fences.ts:42:14 - error SC2020: 'mock.fn of a function returning 'number | string'' is part of the standard library types but has no scriptc lowering yet
37 |
38 | const pick = mock.fn((flag: boolean): number | string => (flag ? 1 : "one")); // union without undefined
41 |
42 | const pick = mock.fn((flag: boolean): number | string => (flag ? 1 : "one")); // union without undefined
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
39 | pick(true);
43 | pick(true);
hint: a union return type needs an undefined arm here (the calls log records undefined for calls that throw)
@@ -0,0 +1 @@
{ "plain": 3028, "san": 5470 }