fix(linter/promise/no-return-wrap): detect Promise calls in all branches#22474
Conversation
Replace manual AST traversal in `check_first_return_statement` with a `Visit`-based approach that walks all branches of control flow (if/else, nested conditionals, else-if chains) to find `return Promise.resolve()` and `return Promise.reject()` wrapping. Previously the rule only checked the first return in the `if` consequent block, missing returns in else branches, nested ifs, and else-if chains. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…urn-wrap Cover additional control flow paths exercised by the Visit-based traversal: switch/case, try/catch, and allowReject option interaction with else branches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes promise/no-return-wrap so it detects return Promise.resolve(...) / return Promise.reject(...) inside promise handler callbacks even when the return is nested under control-flow constructs (e.g. if/else, nested if, switch, try/catch). It replaces a shallow/manual traversal (that also had an else-branch bug) with a visitor that walks the full callback body while stopping at nested function boundaries.
Changes:
- Replace the old “first return/if only” scanning with a
Visit-basedReturnWrapFinderthat traverses the entire callback body and checks allreturnstatements. - Prevent false positives by stopping traversal at nested
function/arrowboundaries. - Add/extend test cases and update the insta snapshot to cover nested branches and the
allowRejectoption.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| crates/oxc_linter/src/rules/promise/no_return_wrap.rs | Reworks callback-body scanning to visitor-based traversal that finds wrapped returns in all nested branches while skipping nested functions. |
| crates/oxc_linter/src/snapshots/promise_no_return_wrap.snap | Updates snapshot output to include newly-detected violations from the added/expanded tests. |
Merging this PR will not alter performance
Comparing Footnotes
|
# Oxlint ### 🚀 Features - 1ae291e linter/no-underscore-dangle: Add `allowInUsingDeclarations` option (#22483) (吴杨帆) - 0440b0f linter/eslint: Implement `id-match` rule (#22379) (Vladislav Sayapin) - 65bf119 linter: Implement react no-object-type-as-default-prop (#22481) (uhyo) - 2a6ddce linter/eslint: Implement `no-implied-eval` rule (#22391) (Vladislav Sayapin) - d3a3c1d linter: Auto detect agents from CLI and transition to the agent output format (#22068) (Jovi De Croock) - 625758a linter/vitest: Implement padding-around-after-all-blocks rule (#21788) (kapobajza) - 37680b0 linter: Implement react no-unstable-nested-components (#22248) (Jovi De Croock) - d8d9c74 linter: Implement import/newline-after-import rule (#19142) (Ryuya Yanagi) ### 🐛 Bug Fixes - 3f59e03 linter: Only call rayon/miette/tracing inits once (#21899) (Matiss Janis Aboltins) - 602dfd6 linter/promise/no-return-wrap: Detect Promise calls in all branches (#22474) (zennnnnnn11) - e182aee linter: Allow dialogs and popovers for no_autofocus (#22289) (mehm8128) - 7ffb710 linter/jest/vitest: Jest/no-standalone-expect ignores additionalTestBlockFunctions option for jest/vitest hooks (#22477) (kapobajza) - c6f2d3f linter: Add more expression support for iframe-has-title (#22460) (mehm8128) - 5747ff1 linter: Avoid enabling jest with vitest plugin (#22499) (camc314) - 863984f linter/no-find-dom-node: Run on all files (#22479) (bab) ### ⚡ Performance - 2afef79 linter: Optimize `no-loop-func` (#22491) (camchenry) - 4c9ca72 oxlint: Align walker thread count with rayon pool (#22494) (Boshen) ### 📚 Documentation - f7967c7 linter/id-match: Clarify `onlyDeclarations` config docs (#22523) (camc314) - 1e0c97f linter: Fix closing code block in documentation for `padding-around-after-all-blocks` rule. (#22513) (connorshea) - a9049fd linter: Exclude directly provide autoFocus to dialog pattern (#22510) (mehm8128) # Oxfmt ### 🐛 Bug Fixes - 8ee946f formatter/sort_imports: Use label to classify lines (#22512) (leaysgur) - 8c1da44 formatter: Normalize destructuring keys in DCR (#22478) (camc314)
Closes #22469
Summary
if/returntraversal incheck_first_return_statementwith aVisit-basedReturnWrapFinderthat walks the entire callback bodycheck_first_return_statemententirelyBefore this fix
The rule only examined the first
returnorifstatement in the callback body. A copy-paste bug also caused the "look in else" fallback to search theifconsequent a second time, never actually checkingalternate. This meant:After this fix
All
return Promise.resolve()/return Promise.reject()statements inside.then(),.catch(), and.finally()callbacks are checked, regardless of nesting depth inif/else,switch,try/catch, etc. The visitor stops at nested function/arrow boundaries to avoid crossing scope.Scope note
The existing
inside_promise_cbgate treats anyCallExpressionnested inside a promise chain as a candidate for argument checking. This is a pre-existing design decision (see thefn3test case) and is not changed by this PR. Expression-body arrows with conditional expressions (e.g.,x => x ? Promise.resolve(x) : x) are also not covered, consistent with the original eslint-plugin-promise behavior — both could be addressed as follow-ups.Test plan
allowRejectoptioncargo test -p oxc_linter -- rules::promise::no_return_wrappassesVisitpattern fromno_promise_executor_return.rs🤖 Generated with Claude Code