feat(rstest): add prefer-to-have-been-called-times rule - #2230
Merged
Merged
Conversation
elecmonkey
force-pushed
the
feat/rstest-prefer-to-have-been-called-times
branch
from
September 20, 2026 18:27
d834e4e to
9f183d3
Compare
Contributor
🦀📦 Binary sizeCommit
Stripped |
elecmonkey
commented
Sep 21, 2026
elecmonkey
left a comment
Member
Author
There was a problem hiding this comment.
I found four correctness issues in the Rstest-specific behavior and autofix boundaries. Inline comments include minimal reproductions and suggested directions.
Stop the matcher walk at Chai matchers that replace the assertion subject, so a toHaveLength after property(0) is no longer reported: it measures the arguments of one recorded call rather than the call count. Restrict the rule to expect() and expect.soft(). expect.poll() takes a callback, so the only shape this rule could match there is an argument that throws before any matcher runs. Remove explicit type arguments on the expect() and matcher calls when rewriting, since both describe the subject and matcher being replaced, and withhold the fix when a comment sits inside either list.
Both rstest/prefer-to-have-length and rstest/prefer-to-have-been-called-times stop their matcher walk at the Chai matchers that replace the assertion object's value. Move the table behind IsSubjectMutatingChaiMatcher so the two rules cannot drift apart, and cover it with a unit test.
elecmonkey
marked this pull request as ready for review
September 21, 2026 04:20
fansenze
reviewed
Sep 21, 2026
fansenze
left a comment
Contributor
There was a problem hiding this comment.
Three remaining autofix issues, reproduced on this revision with Rstest 0.11.11. The existing targeted Go tests and rule registration check pass; additional regression probes confirm the issues below.
…nge results
toHaveLength compares loosely and toHaveBeenCalledTimes strictly, so rewriting
toHaveLength('1') makes a passing assertion fail and rewriting
not.toHaveLength('1') makes a failing one pass. Offer the fix only when the
expected count is a number written in source.
expect() captures the calls array, while toHaveBeenCalledTimes reads mock.calls
when the matcher runs. Anything evaluated in between can reset the mock, so
require the remaining expect() arguments to be literals as well.
A bare super is not a value, so expect(super.mock.calls) cannot be rewritten to
expect(super) without producing code that does not parse.
All three cases keep their diagnostic and lose only the autofix. Move the
numeric-literal predicate shared with rstest/prefer-to-have-length into
test_framework as StaticNumericLiteral.
fansenze
approved these changes
Sep 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add
rstest/prefer-to-have-been-called-timesto reporttoHaveLength()assertions on a mock'smock.callsarray and rewrite them totoHaveBeenCalledTimes(). It is registered in the Rstest plugin without being added to therecommendedpreset, matching upstream.The table of Chai matchers that replace the assertion subject and the predicate for a numeric literal written in source move to
internal/utils/test_framework, behindIsSubjectMutatingChaiMatcherandStaticNumericLiteral, shared withrstest/prefer-to-have-lengthso the two rules cannot drift apart.Credits
Port from
eslint-plugin-jest'sprefer-to-have-been-called-times.Intentional differences from upstream
expect(handler[mock].calls)reads a variable rather than themockproperty, so reporting and rewriting it would be a false positive.expect(handler.mock.calls)[matcher](2)names its matcher through a variable, which the assertion parser does not resolve totoHaveLength.expect((handler.mock).calls).toHaveLength(2)into unparseable code.?.inexpect(handler.mock.calls)?.toHaveLength(2).expect(handler.mock.calls)["toHaveLength"](2)to a dotted accessor; this port changes only the literal value.expect.softassertions are checked. Soft assertions record the same call count, so they are treated like ordinary assertions.expect(handler.mock.calls).property(0).toHaveLength(2)measures the arguments of one recorded call rather than the call count, so the chain ends atproperty.expect.pollassertions are not checked.expect.polltakes a callback, so the only shape this rule could match there is an argument that throws before any matcher runs.toHaveLengthcompares loosely andtoHaveBeenCalledTimesstrictly, so rewritingtoHaveLength('1')fails an assertion that passed and rewritingnot.toHaveLength('1')passes one that failed.expect()captures the calls array whiletoHaveBeenCalledTimesreadsmock.callswhen the matcher runs, soexpect(handler.mock.calls, (handler.mockClear(), 'called once'))would change result after the rewrite.superreceiver is reported without a fix. A baresuperis not a value, soexpect(super.mock.calls)cannot becomeexpect(super)without producing code that does not parse.Known gaps
expect.extendoverrides are not consulted. A file-local override oftoHaveLengthortoHaveBeenCalledTimeschanges what either matcher means, and no matcher-rewrite rule in the plugin checksIsExpectMatcherOverriddentoday; making this one the exception would be inconsistent, so it is left for a separate change covering the whole family.Related Links
Related #935
Checklist