fix(eslint-factory): treat try/finally without catch as unprotected in isInsideTryBlock#44256
Conversation
…arse rules A try/finally without catch does not handle the error — the throw still propagates. Only treat a TryStatement as protective when it has a catch handler (ancestor.handler != null). try/catch/finally remains protective. Add tests for both rules: - invalid: try/finally without catch is flagged - valid: try/catch/finally is treated as protective Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a correctness issue in the eslint-factory rules’ isInsideTryBlock logic: a try/finally without a catch is no longer treated as an error-handling boundary, so sync fs.*Sync(...) calls and JSON.parse(...) inside such blocks are correctly flagged as unprotected.
Changes:
- Update
isInsideTryBlockin both rules to only treatTryStatementnodes with a non-nullhandler(i.e., an actualcatch) as protective. - Add/extend tests to confirm
try/catch/finallyremains protective whiletry/finally(no catch) is now invalid for both rules.
Show a summary per file
| File | Description |
|---|---|
| eslint-factory/src/rules/require-json-parse-try-catch.ts | Require ancestor.handler != null for a try to be considered protective. |
| eslint-factory/src/rules/require-json-parse-try-catch.test.ts | Add coverage for try/catch/finally (valid) and try/finally (invalid) cases. |
| eslint-factory/src/rules/require-fs-sync-try-catch.ts | Require ancestor.handler != null for a try to be considered protective. |
| eslint-factory/src/rules/require-fs-sync-try-catch.test.ts | Add coverage for try/catch/finally (valid) and try/finally (invalid) cases. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 4/4 changed files
- Comments generated: 0
- Review effort level: Low
This comment has been minimized.
This comment has been minimized.
|
Hey A couple of things to address before this is ready for review:
If you'd like a hand, you can assign this prompt to your coding agent:
|
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
|
✅ Test Quality Sentinel completed test quality analysis. |
|
|
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR #44256 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100). |
🧪 Test Quality Sentinel Report✅ Test Quality Score: 90/100 — Excellent
📊 Metrics (4 tests)
Verdict
Summary: Tests directly validate that Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
There was a problem hiding this comment.
The fix is correct and well-tested.
TryStatement.handler in the ESTree AST is the catch clause node — it is null when there is no catch, which is exactly the right discriminator. A try/finally only guarantees cleanup code runs; the thrown error is re-thrown after finally completes, so the call is effectively unprotected.
Both rules are updated symmetrically, and the new test cases cover the two critical scenarios:
try/finally(no catch) — correctly flagged as invalidtry/catch/finally— correctly accepted as valid
No issues found.
🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 22.7 AIC · ⌖ 6.1 AIC · ⊞ 4.8K
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /diagnosing-bugs and /tdd — approving with one minor documentation suggestion.
📋 Key Themes & Highlights
Root Cause
The isInsideTryBlock predicate was checking only ancestor.type === "TryStatement", but a try/finally without a catch clause does not handle exceptions — the throw resumes propagating after finally runs. The fix correctly gates on ancestor.handler != null, which is the ESTree field for the catch clause.
Positive Highlights
- ✅ Minimal, surgical fix — 1-line change per rule, no logic restructuring needed
- ✅ Symmetric fix across both
require-fs-sync-try-catchandrequire-json-parse-try-catch - ✅ Tests cover both directions:
try/finally(now invalid) andtry/catch/finally(still valid) - ✅ Auto-fix suggestion output is verified in the new test cases — wraps the call inside a new
try/catchrather than discarding the outerfinally
Minor Observation
- The
isInsideTryBlockfunction comment in both files doesn't mention thehandler != nullinvariant. See inline comments for a suggested documentation addition — this is non-blocking.
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 53.8 AIC · ⌖ 6.66 AIC · ⊞ 6.6K
Comment /matt to run again
| } | ||
|
|
||
| if (ancestor.type === "TryStatement" && !crossedDeferredBoundary) { | ||
| if (ancestor.type === "TryStatement" && !crossedDeferredBoundary && ancestor.handler != null) { |
There was a problem hiding this comment.
[/diagnosing-bugs] The handler != null guard is correct, but the function's existing comment doesn't document the catch-clause contract. A future reader seeing isInsideTryBlock return false for a try/finally block may be confused without this context.
💡 Suggested comment addition
// Walk from innermost ancestor outward. Stop marking a try as protective once a deferred
// callback boundary has been crossed (the callback fires after the try has returned).
// NOTE: only TryStatements with a catch clause (handler != null) are treated as protective;
// a bare try/finally does not catch exceptions — the throw continues propagating after finally.
let crossedDeferredBoundary = false;This makes the protection contract self-documenting and prevents future contributors from widening the check back to handler == null cases.
@copilot please address this.
| } | ||
|
|
||
| if (ancestor.type === "TryStatement" && !crossedDeferredBoundary) { | ||
| if (ancestor.type === "TryStatement" && !crossedDeferredBoundary && ancestor.handler != null) { |
There was a problem hiding this comment.
[/diagnosing-bugs] Same handler != null fix as the fs rule — identical concern about the missing comment. Consider adding the same note here to keep both implementations self-consistent in their documentation.
💡 Suggested comment addition
// Walk from innermost ancestor outward. If we cross a deferred function boundary
// (e.g., a .then/.on/setTimeout callback), a try statement further out does NOT
// protect the node — the callback runs after the try has already returned.
// NOTE: only TryStatements with a catch clause (handler != null) are treated as protective;
// a bare try/finally does not catch exceptions — the throw continues propagating after finally.
let crossedDeferredBoundary = false;@copilot please address this.
There was a problem hiding this comment.
Verdict: APPROVE ✅
The fix is correct and well-targeted. Grumpy-coder sub-agent found nothing; my independent pass agrees.
Review notes
What the fix does
TryStatement.handler is the AST node for the catch clause. Before this fix, isInsideTryBlock returned true for any enclosing TryStatement, including try/finally with no catch. A catch-less try/finally lets the exception propagate after cleanup — so the original code was a false negative. The one-liner guard ancestor.handler != null is the correct, minimal fix.
The change is symmetric across both rules (require-fs-sync-try-catch and require-json-parse-try-catch), and the new test cases confirm both the fixed invalid case and the still-valid try/catch/finally case.
Minor observation (non-blocking)
The new invalid: try/finally without catch test in require-fs-sync-try-catch.test.ts covers writeFileSync and readFileSync, but not appendFileSync (which is also in FS_SYNC_METHODS). The rule logic is method-agnostic so correctness is unaffected, but a third case would complete coverage parity with the full method set.
🔎 Code quality review by PR Code Quality Reviewer · 151.8 AIC · ⌖ 6.21 AIC · ⊞ 5.4K
Comment /review to run again
|
🎉 This pull request is included in a new release. Release: |
isInsideTryBlockwas returningtruefor any enclosingTryStatement, includingtry/finallywith nocatch. A catch-lesstry/finallyonly runs cleanup — the throw resumes propagating afterfinally, so the fs/JSON.parse call is still unhandled.Changes
require-fs-sync-try-catch.ts/require-json-parse-try-catch.ts: Gate the protective check onancestor.handler != null, so onlytry/catchandtry/catch/finallyare treated as error-handling boundaries;try/finallyalone is not.try { fs.writeFileSync(p, d); } finally { cleanup(); }— now flaggedtry { fs.writeFileSync(p, d); } catch (e) {} finally { cleanup(); }— still passes