Skip to content

eslint-factory: extend no-empty-catch-block to empty Promise .catch handlers - #52979

Merged
pelikhan merged 3 commits into
mainfrom
copilot/eslint-factory-fix-no-empty-catch-coverage
Aug 15, 2026
Merged

eslint-factory: extend no-empty-catch-block to empty Promise .catch handlers#52979
pelikhan merged 3 commits into
mainfrom
copilot/eslint-factory-fix-no-empty-catch-coverage

Conversation

Copilot AI commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

no-empty-catch-block previously only analyzed CatchClause nodes, so empty Promise terminal handlers like .catch(() => {}) could silently swallow rejections with no diagnostics. This closes that gap while preserving explicit intent-based exemptions for documented non-fatal swallowing patterns.

  • Rule scope expansion

    • Added a CallExpression selector for .catch(...) member calls.
    • Flags inline handler functions with an empty block body:
      • .catch(() => {})
      • .catch(function () {})
    • Non-empty catch handlers remain unaffected.
  • Shared intent-comment exemption logic

    • Refactored comment handling into shared helpers used by both try/catch and Promise .catch.
    • Preserves exemption behavior for intentional ignores via adjacent/inside comments.
    • Expanded intent vocabulary to include non-fatal and silently swallowed (in addition to existing intentional/best-effort forms), so documented entrypoint swallow sites remain valid.
  • Coverage updates

    • Added invalid cases for undocumented empty Promise catch handlers.
    • Added valid cases for:
      • non-empty Promise catch handlers
      • documented intentional empty Promise catch handlers (including “Non-fatal: errors are silently swallowed.”).
// now flagged
run().catch(() => {});

// allowed with intent signal
// Non-fatal: errors are silently swallowed.
if (require.main === module) {
  run().catch(() => {});
}

Copilot AI and others added 2 commits August 15, 2026 22:22
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix no-empty-catch-block coverage for empty Promise catch handlers eslint-factory: extend no-empty-catch-block to empty Promise .catch handlers Aug 15, 2026
Copilot AI requested a review from pelikhan August 15, 2026 22:25
@pelikhan
pelikhan marked this pull request as ready for review August 15, 2026 22:29
Copilot AI balanced review requested due to automatic review settings August 15, 2026 22:29
@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Ponytail Reviewer completed successfully!

Generated by Ponytail Reviewer for #52979

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR does not have the implementation label and has 0 new lines of code in business logic directories.

🏗️ ADR gate enforced by Design Decision Gate 🏗️

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Extends the ESLint rule to detect undocumented empty Promise rejection handlers.

Changes:

  • Adds .catch(() => {}) and function-handler detection.
  • Shares intentional-ignore comment handling across catch forms.
  • Adds valid and invalid rule tests.
Show a summary per file
File Description
eslint-factory/src/rules/no-empty-catch-block.ts Implements Promise catch detection and comment exemptions.
eslint-factory/src/rules/no-empty-catch-block.test.ts Covers Promise handler diagnostics and exemptions.

Review details

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 2/2 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment on lines +40 to +47
const ancestors = sourceCode.getAncestors(node);
for (let i = ancestors.length - 1; i >= 0; i -= 1) {
const ancestor = ancestors[i];
if (ancestor.type === AST_NODE_TYPES.Program) break;
if (ancestor.type.endsWith("Statement") && hasAdjacentIntentionalIgnoreComment(ancestor)) {
return true;
}
}
`run().catch((err) => { core.warning(getErrorMessage(err)); });`,
`run().catch(function (err) { core.warning(getErrorMessage(err)); });`,
`run().catch(() => { /* best-effort cleanup */ });`,
`// Non-fatal: errors are silently swallowed.\nif (require.main === module) {\n run().catch(() => {});\n}`,
messageId: "noEmptyCatch",
});
},
"CallExpression[callee.type='MemberExpression'][callee.property.type='Identifier'][callee.property.name='catch']"(node: TSESTree.CallExpression) {
@github-actions

Copy link
Copy Markdown
Contributor

Comment Memory

reviewed_at: 2026-08-15T00:00:00Z
review_event: REQUEST_CHANGES
top_themes:
  - overly broad ancestor comment exemption creates false negatives
files_reviewed:
  - eslint-factory/src/rules/no-empty-catch-block.ts
  - eslint-factory/src/rules/no-empty-catch-block.test.ts
comment_count: 1

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 3.82 AIC · ⌖ 6.75 AIC · ⊞ 6.9K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request changes

This broadens the rule usefully, but the new ancestor-comment exemption makes it too easy for unrelated enclosing comments to silence genuinely undocumented empty handlers.

Blocking theme
  • The rule now treats comments on enclosing statements as intent for nested empty catch / .catch handlers, which creates false negatives and weakens the lint rule's core guarantee.

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 3.82 AIC · ⌖ 6.75 AIC · ⊞ 6.9K
Comment /review to run again

if (hasAdjacentIntentionalIgnoreComment(block)) return true;
if (hasAdjacentIntentionalIgnoreComment(node)) return true;

const ancestors = sourceCode.getAncestors(node);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This comment-ancestor walk is too broad: a generic // Non-fatal... comment on any enclosing statement now suppresses diagnostics for nested empty catch {} blocks and .catch(() => {}), so the rule can be bypassed accidentally by unrelated control-flow comments.

💡 Why this is a real correctness hole

hasIntentionalIgnoreComment() climbs every ancestor whose type ends with Statement and accepts any adjacent comment there. That means code like this now passes even though the empty handler itself is undocumented:

// Non-fatal: continue startup if optional step fails
if (ready) {
  try {
    risky();
  } catch {}
}

The comment is about the if statement, not necessarily the empty catch body, but the rule treats it as authorization to swallow any nested error. That weakens the lint signal exactly where it is supposed to be strict.

Please tighten the exemption to comments attached to the handler itself (inside the block, immediately before the catch clause / callback, or another deliberately narrow scope), and add a regression test proving an enclosing statement comment does not exempt a nested empty handler.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The extension looks correct and well-tested. The new AST selector accurately targets empty .catch handlers, the comment-based escape hatch works for both inline and ancestor-level comments, and all new test cases align with the implementation. No issues found.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 12.5 AIC · ⌖ 7 AIC · ⊞ 5.6K

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ponytail review (over-engineering only).

net: -6 lines possible.

Generated by ✂️ Ponytail Reviewer for #52979 · auto · 14.4 AIC · ⌖ 3.14 AIC · ⊞ 7.2K
Comment /ponytail to run again

context.report({
node: node.body,
messageId: "noEmptyCatch",
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

L64-71: yagni: unbounded ancestor walk up to Program just to support one test case (comment above wrapping if). Check only the immediate parent statement instead of looping through all ancestors.

@github-actions

Copy link
Copy Markdown
Contributor

Test Quality Sentinel 🧪 — PR #52979

Test Quality Score: ✅ 100/100 Excellent

Summary

This PR extends the no-empty-catch-block ESLint rule to detect empty Promise .catch() handlers in addition to try-catch blocks. The test changes provide comprehensive coverage of the new feature with no quality concerns.

Key Findings

Test Coverage: 6 new test cases (4 valid, 2 invalid)

  • ✅ 100% design tests (all verify observable rule behavior)
  • ✅ 100% edge-case coverage (arrow + named functions, comments, external signals)
  • ✅ 0% implementation tests (no false-comfort coverage)
  • ✅ No mock violations or hard violations

Test Inflation Ratio: 0.27:1 (12 test lines / 44 prod lines)

  • Well below 2:1 threshold — minimal bloat

Assertion Quality: All tests use RuleTester's messageId assertions to verify rule acceptance/rejection

Detailed Test Classification
Test Pattern Design Test Edge Cases Duplication
Valid: catch((err) => { core.warning(...) }) Arrow + logging
Valid: catch(function (err) { core.warning(...) }) Named function + logging
Valid: catch(() => { /* best-effort cleanup */ }) Arrow + intent comment
Valid: Adjacent // Non-fatal: ... comment External comment pattern
Invalid: catch(() => {}) Empty arrow (should error)
Invalid: catch(function () {}) Empty named (should error)

Rationale

The new test cases directly verify the feature requirement: the rule now rejects empty Promise .catch() handlers (with the same comment-intent allowances as try-catch). Each test pattern covers a distinct syntax variant or comment behavior — no redundant assertions or duplicated setup.


✅ Recommendation: APPROVE
Implementation tests: 0% (threshold: ≤30%)
No hard violations or quality concerns.

🧪 Test quality analysis by Test Quality Sentinel · haiku45 · 15 AIC · ⌖ 4.07 AIC · ⊞ 7.9K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 100/100 Excellent. 0% implementation tests (threshold: ≤30%). No hard violations — comprehensive design tests covering new Promise .catch() handler detection feature with proper edge-case coverage.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /tdd and /codebase-design — requesting changes on two correctness gaps and one test coverage gap.

📋 Key Themes & Highlights

Key Themes

  • False positive risk (correctness): The new CallExpression selector is not scoped to Promise-returning calls. Any object with a .catch method (cache, event emitter, test double) will trigger the rule incorrectly.
  • Ancestor-walk scope creep (correctness): The intent-comment ancestor walk is unbounded — a comment above a distant ancestor statement can exempt an unrelated inner catch block. Should be capped at a defined ancestor depth.
  • Missing cross-path test coverage (test coverage): The new regex vocabulary (non-fatal, silently swallow*) was added to the shared helper but is only tested in the Promise .catch path. try/catch valid-case tests for the new terms are absent.

Positive Highlights

  • ✅ Clean extraction of comment-handling logic into shared helpers — good deep-module design
  • ✅ Solid invalid-case tests for the new Promise path
  • ✅ Backward-compatible: no changes to existing try/catch error messages or rule ID

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 34.9 AIC · ⌖ 7.71 AIC · ⊞ 7.7K
Comment /matt to run again

});
},
"CallExpression[callee.type='MemberExpression'][callee.property.type='Identifier'][callee.property.name='catch']"(node: TSESTree.CallExpression) {
const [handler] = node.arguments;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] The selector matches any .catch(handler) call, not just on Promises. Objects with a custom catch method (e.g. a Cache or test double) will produce false positives.

💡 Suggested guard

Document the known limitation, and add a valid fixture covering a non-Promise object:

// valid – non-Promise object with its own .catch method
`const myObj = { catch: (fn) => fn() }; myObj.catch(() => {})`,

If that should be flagged, that’s fine — but the rule docs should say so. If not, narrow the selector or add an explicit exclusion.

@copilot please address this.

if (ancestor.type.endsWith("Statement") && hasAdjacentIntentionalIgnoreComment(ancestor)) {
return true;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/codebase-design] hasIntentionalIgnoreComment walks ancestors and breaks on any *Statement node with a preceding intent comment, but the loop stops at Program. A chained call deep inside a function body could legitimately pick up an intent comment on a sibling IfStatement that was meant for a different block. The proximity guard is only applied to the adjacent line, so a comment two lines above an unrelated sibling would still trigger the exemption.

💡 Consider a tighter ancestor scope

Consider limiting the ancestor walk to the nearest enclosing ExpressionStatement (the direct parent of the call) rather than any ancestor *Statement. That matches the documented exemption pattern:

// Non-fatal: errors are silently swallowed.
if (require.main === module) {
  run().catch(() => {}); // <-- intent comment is on the IfStatement
}

Walking up to the IfStatement is deliberate here, but it should be bounded so that an intent comment on a distant ancestor ForStatement is not accidentally honoured.

@copilot please address this.

@@ -27,14 +56,26 @@ export const noEmptyCatchBlockRule = createRule({
// An explicit intentional-ignore comment inside the otherwise empty
// braces documents intent, e.g.:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] The original CatchClause handler now delegates to hasIntentionalIgnoreComment, but the old test cases for try/catch only covered /* intentional */ and /* best-effort */ patterns. The expanded regex now also matches non-fatal and silently swallow* — there are no tests asserting that those new patterns also exempt a try/catch block.

💡 Missing test cases to add
// valid – new vocabulary in try/catch context
`try { risky(); } catch { /* non-fatal: optional step */ }`,
`try { risky(); } catch { /* errors are silently swallowed */ }`,

Without these, a future regex change could silently break the try/catch path.

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot Quick triage for maintainer-ready follow-up:

Please refresh the branch if needed, address the remaining maintainer-facing follow-up, and run the pr-finisher skill before handing this PR back to maintainers.

Outstanding review items (newest first):

Failed checks from the compact candidate set:

  • None listed.

Branch update was requested automatically for this run when GitHub allows it.
Run context: https://github.com/github/gh-aw/actions/runs/31912931553

Generated by 👨🍳 PR Sous Chef
Comment /souschef to run again

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 8.98 AIC · ⌖ 7.62 AIC · ⊞ 8.7K ·
Comment /souschef to run again

@pelikhan
pelikhan merged commit 5a5f8ed into main Aug 15, 2026
47 of 48 checks passed
@pelikhan
pelikhan deleted the copilot/eslint-factory-fix-no-empty-catch-coverage branch August 15, 2026 22:55
Copilot stopped work on behalf of gh-aw-bot due to an error August 15, 2026 22:56
Copilot AI requested a review from gh-aw-bot August 15, 2026 22:56
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.87.0

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

eslint-factory: no-empty-catch-block has no coverage for empty Promise .catch(() => {}) entrypoint handlers

4 participants