Skip to content

Eslint disposable test hooks#49

Merged
kentcdodds merged 13 commits intomainfrom
cursor/eslint-disposable-test-hooks-a299
Feb 13, 2026
Merged

Eslint disposable test hooks#49
kentcdodds merged 13 commits intomainfrom
cursor/eslint-disposable-test-hooks-a299

Conversation

@kentcdodds
Copy link
Member

@kentcdodds kentcdodds commented Feb 13, 2026

Add epic-web/prefer-dispose-in-tests ESLint rule to enforce using dispose and disposeAsync over before*/after* hooks in tests.

This rule encourages a more robust and explicit resource management pattern in tests, as described in the EpicWeb.dev article "Better Test Setup with Disposable Objects," by flagging traditional hooks when a disposable pattern is feasible. It includes heuristics to automatically allow exceptions where dispose is not reasonably possible (e.g., callback parameters, this context, outer state mutation, multi-test suite-level hooks, or known framework-wide timer/mock hooks).


Open in Cursor Open in Web


Note

Medium Risk
Introduces a new, enabled-by-default lint rule for all test files, which can surface new warnings and relies on AST/scope heuristics that may have edge-case false positives/negatives.

Overview
Adds a new lint rule, epic-web/prefer-dispose-in-tests, that warns on beforeEach/afterEach/beforeAll/afterAll usage in test suites when setup/cleanup can be moved into per-test disposable setup (using/await using), with heuristics to skip common non-trivial cases (callback params/this, outer-state mutation, setup-only files, known timer/mock helpers, and larger suites).

Registers the rule in the epic-web plugin, exports a dedicated ./eslint-plugin entrypoint, enables the rule as warn for test-file overrides in both eslint.js and oxlint-config.json, and adds docs plus a RuleTester suite covering allowed/reported scenarios.

Written by Cursor Bugbot for commit 1ca4870. This will update automatically on new commits. Configure here.

@cursor
Copy link

cursor bot commented Feb 13, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@kentcdodds kentcdodds marked this pull request as ready for review February 13, 2026 19:59
@cursor

This comment has been minimized.

@cursor

This comment has been minimized.

@cursor
Copy link

cursor bot commented Feb 13, 2026

Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.

  • ✅ Fixed: Framework hook exception misses suite hooks
    • Removed the suite-hook guard so known framework hooks are exempt for suite hooks as intended.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outer mutation heuristic misses method calls

Low Severity

writesOuterState only treats AssignmentExpression and UpdateExpression as mutations. Hooks that mutate shared outer state through method calls like array.push(...), map.set(...), or mutating helpers are not recognized, so epic-web/prefer-dispose-in-tests reports hooks that the rule intends to exempt for outer-state mutation.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-test APIs counted as test cases

Medium Severity

isTestCallExpression treats almost any test.*(...) call as a test, so helper APIs like test.use, test.extend, or similar configuration calls inflate testCount. This breaks suite analysis in analyzeSuiteNode, causing hook warnings to be emitted or suppressed based on non-test calls.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test counting includes non-executed test calls

Low Severity

analyzeSuiteNode counts every test/it call anywhere under the suite AST, including calls inside nested helper functions. That inflates testCount, so minimumTestsForSuiteHooks exemptions can trigger even when the suite has too few runnable tests, causing epic-web/prefer-dispose-in-tests to miss reports.

Fix in Cursor Fix in Web

if (currentNode.type === 'CallExpression' && isTestCallExpression(currentNode)) {
testCount += 1
}
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suite test counting includes non-executed definitions

Low Severity

analyzeSuiteNode counts every test/it call found anywhere under the suite AST. That includes calls inside nested helper functions or other non-executed scopes, which can inflate testCount and incorrectly skip reports for lifecycle hooks in suites that do not actually have enough runnable tests.

Fix in Cursor Fix in Web


function getHookCallback(node) {
return node.arguments.find((argument) => isFunctionNode(argument)) ?? null
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function reference hooks bypass the rule

Low Severity

getHookCallback only accepts inline FunctionExpression and ArrowFunctionExpression arguments. Hooks written as beforeEach(setupFn) return null, and create exits early, so epic-web/prefer-dispose-in-tests never evaluates or reports those hook usages.

Additional Locations (1)

Fix in Cursor Fix in Web

cursoragent and others added 12 commits February 13, 2026 20:33
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@cursor cursor bot force-pushed the cursor/eslint-disposable-test-hooks-a299 branch from cf2a7e7 to 61f9607 Compare February 13, 2026 20:37
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

writeTarget = currentNode.left
} else if (currentNode.type === 'UpdateExpression') {
writeTarget = currentNode.argument
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outer mutation heuristic misses method-based writes

Low Severity

writesOuterState only treats AssignmentExpression and UpdateExpression as outer-state writes. Hooks that mutate captured objects via calls like arr.push(...), map.set(...), or obj.method(...) are not recognized, so epic-web/prefer-dispose-in-tests can report hooks that actually rely on shared outer mutation.

Fix in Cursor Fix in Web

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kentcdodds kentcdodds merged commit 913d51f into main Feb 13, 2026
5 checks passed
@kentcdodds kentcdodds deleted the cursor/eslint-disposable-test-hooks-a299 branch February 13, 2026 20:46
@github-actions
Copy link

🎉 This PR is included in version 1.24.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments