require-mock-type-parameters
Added in v0.9.1Configuration
Rule Details
Requires rs.fn() to be given a type parameter. With nothing passed for the compiler to infer a signature from, the mock falls back to one that accepts any arguments and returns any, so nothing about how the test drives it is checked: a call with the wrong arguments, a mockResolvedValue of the wrong shape, and an assertion against a property the real function never returns all pass the compiler. Writing the signature — rs.fn<(id: number) => Promise<User>>() — makes the mock stand in for the function it replaces, in the editor as well as at build time.
A call that passes an implementation is reported too, even though the compiler does infer a signature from it: rs.fn((value: number) => value.toString()) is a Mock<(value: number) => string>. That signature describes the throwaway body the test happened to write rather than the function the mock stands in for, so it moves whenever the body does — relaxing a parameter to unknown, or writing the () => {} a stub usually is, quietly relaxes what every later argument list, mockReturnValue and assertion is checked against, and the second of those checks nothing at all. The type parameter states the contract once, and the implementation is then checked against it as well.
fn is an ordinary function on the utilities object, so the rule follows the binding: it is reported whether the receiver came from an import of @rstest/core or rstack/test under either of its names, a require of either, a further rename such as import { rs as mocker }, a namespace as in core.rs.fn(), or the globals Rstest installs. A receiver the file declares itself — a local object, a function parameter, a binding imported from somewhere else — is a different object and is left alone. The call has to go through a plain dotted member, so rs['fn']() is not reported, and rs.fn read as a value without being called carries no call to write a type parameter on.
With checkImportFunctions enabled the rule also covers the four APIs that load a module: importActual, importMock, requireActual and requireMock. Each returns Record<string, unknown> unless told what the module exports, so the result has no named exports and nothing callable on it. These four are rewritten by Rstest's build rather than called, and the build matches the receiver on the name written at the call site: it must be spelled rs or rstest, so a renamed binding and a namespace are left as they are, because Rstest leaves them alone too and they throw where they stand. An optional receiver, rs?.importActual('./dep'), is never rewritten and never reported.
How the member itself may be written differs between the two pairs, because the build reads them differently. importActual and requireActual are also rewritten through a bracketed string key or an optional call, so rs['importActual']('./dep') and rs.importActual?.('./dep') are reported alongside rs.importActual('./dep'); and a receiver the file declares itself — a local object, a function parameter — takes the call back, so it is not reported. importMock and requireMock are rewritten only as a plain dotted member: rs['importMock']('./dep') and rs.importMock?.('./dep') throw where they stand and are not reported, while a receiver the file declares itself does not stop the rewrite, so rs.importMock('./dep') is still reported there.
A call the build cannot rewrite is never reported: a path that is not a quoted string, or any argument count other than one.
Parentheses and TypeScript's type-only syntax are transparent throughout: (rs as any).fn(), rs!.fn() and (rs.fn)() are reported exactly like the bare form. A call that already carries a type argument is what the rule asks for and is never reported.