prefer-todo
UnreleasedConfiguration
Rule Details
Prefer test.todo(...) over Rstest test registrations that are present in the
file but still have no implementation. The rule reports two cases:
- the callback is missing entirely;
- the callback is an inline empty function body.
Unlike Jest, Rstest accepts both (title, fn, timeout) and
(title, options, fn?). This rule recognizes the object-literal options form
and keeps the options object when it rewrites the registration to .todo.
Examples of incorrect code for this rule:
Examples of correct code for this rule:
Limitations
Reporting and autofixing are separate decisions, so a registration can be reported with no fix attached.
Reported without a fix
A registration the parser has proved to be an Rstest test is always reported, even when no rewrite delivers a todo test:
- a same-file
constalias that hides.skipin its initializer, such asconst skipped = test.skip; skipped('case'), because the call site has no accessor to replace; - a repeated
.skipchain, such astest.skip.skip('case', () => {}), because replacing one accessor leaves the other skip active; - an optional registration such as
test?.('case'), because inserting.todowould move the optional boundary and may change the call's runtime behavior; - a call with unsupported arguments whose callback is still provably empty,
such as
test('case', () => {}, 'not a timeout')ortest('case', () => {}, 1000, extra), because dropping or preserving those arguments would guess at behavior outside Rstest's overloads.
Not reported at all
The rule stays silent when it cannot prove the call still reaches the Rstest test API, or when the run mode it would have to preserve is unknown:
- whole-module CommonJS namespace objects, such as
const core = require('@rstest/core'); core.test('case'). Their API properties are mutable even when the namespace binding is declared withconst, socore.testmay no longer be Rstest; - aliases that hide a modifier or factory other than
.skip, such as.fails,.each(...),.for(...)or.extend(...), whose run mode the rewrite would have to reason about; - computed member access such as
test[skip]('case', () => {}). The member is not statically known, so the run mode may be one the rule does not report on at all, such as.onlyor.each.
Sources the rule does follow are Rstest globals, named imports, ES module
namespace imports, import.meta.rstest including const object destructuring
of it, and same-file const aliases whose every hop stays a bare test API.
Overload recognition
The options overload is recognized only when the second argument is written as
an object literal. Calls such as test('case', options) and
test('case', options, () => {}) are left unchanged because the identifier may
still be the callback.
Rstest-specific behavior
The rule intentionally differs from upstream in these cases:
- interpolated template titles such as
test(`case ${name}`)are reported, because the dynamic title does not make a missing or empty callback ambiguous; - nested registrations such as
wrap(test('case'))are reported, because the innertest(...)call still executes and registers an Rstest test. Rewriting that inner call totest.todo(...)preserves its registration role; - parentheses around an inline empty callback are removed together with the callback. Upstream's fix range ends at the inner function and can leave unmatched closing parentheses, while this rule keeps the fixed call parseable.