Skip to content

fix(eslint-factory): extend no-caught-error-interpolation to cover EventEmitter .on('error') handlers - #49768

Merged
pelikhan merged 2 commits into
mainfrom
copilot/eslint-factory-fix-no-caught-error-interpolation
Aug 2, 2026
Merged

fix(eslint-factory): extend no-caught-error-interpolation to cover EventEmitter .on('error') handlers#49768
pelikhan merged 2 commits into
mainfrom
copilot/eslint-factory-fix-no-caught-error-interpolation

Conversation

Copilot AI commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

no-caught-error-interpolation only recognized try/catch and promise .catch/.then rejection handlers as "caught error" scopes, missing the structurally identical EventEmitter pattern — emitter.on('error', err => \...${err}...`)— where Node guarantees the callback receives anError` object.

Changes

  • no-caught-error-interpolation.ts

    • Added isInlineEventErrorHandler(): mirrors isInlineRejectionHandler() but matches .on/.once/.addListener calls where the first argument is the string literal "error" and the function is the second argument
    • Updated isCaughtErrorVariableDef() to call the new helper, gating on fnNode.params[0] === def.name so only the first parameter (the error argument) is flagged — not any additional params
    • Updated rule docs.description to list EventEmitter handlers as a detected scope
  • no-caught-error-interpolation.test.ts — 5 new cases:

    • Valid: non-'error' event names (.on('data', ...)) are not flagged
    • Valid: named/hoisted function reference passed to .on('error', onError) is not flagged (inline-only restriction, now explicitly documented)
    • Invalid: .on('error', ...), .once('error', ...), .addListener('error', ...) — all flagged with String(err) suggestion

Grounded repro (mcp_server_core.cjs:1052) is now a confirmed true positive:

// now flagged — suggests String(err)
process.stdin.on("error", err => server.debug(`stdin error: ${err}`));

…entEmitter .on('error') handlers

- Add isInlineEventErrorHandler() helper that matches .on/'once'/.addListener with 'error' event
- Update isCaughtErrorVariableDef() to recognize first param of inline EventEmitter error listeners
- Update rule description to mention EventEmitter scope
- Add 5 new tests: valid (non-error events, named listener), invalid (.on/.once/.addListener + mcp_server_core.cjs repro)

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix no-caught-error-interpolation for EventEmitter error handlers fix(eslint-factory): extend no-caught-error-interpolation to cover EventEmitter .on('error') handlers Aug 2, 2026
Copilot AI requested a review from pelikhan August 2, 2026 11:55
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

PR Triage

  • Category:
  • Risk:
  • Priority: (score: 35/100)
    • Impact: 16/50, Urgency: 10/30, Quality: 9/20
  • Recommended action:

Automated triage — see full report issue for details.

Structured data:

{
  "action": "defer",
  "category": "bug",
  "pr_number": 49768,
  "risk": "low"
}

Generated by 🔧 PR Triage Agent · auto · 73.4 AIC · ⌖ 3.47 AIC · ⊞ 8K ·

@pelikhan
pelikhan marked this pull request as ready for review August 2, 2026 12:56
Copilot AI review requested due to automatic review settings August 2, 2026 12:56
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Aug 2, 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 ≤100 new lines of code in business logic directories (0 additions in default business logic dirs).

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 unsafe interpolation in inline EventEmitter error handlers.

Changes:

  • Recognizes .on(), .once(), and .addListener() error handlers.
  • Documents the expanded detection scope.
  • Adds positive and negative RuleTester coverage.
Show a summary per file
File Description
no-caught-error-interpolation.ts Adds EventEmitter error-handler detection.
no-caught-error-interpolation.test.ts Tests supported methods and exclusions.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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

if (isInlineRejectionHandler(fnNode)) return true;
if (isInlineEventErrorHandler(fnNode)) {
// Only the first parameter receives the error object from an 'error' event
return fnNode.params[0] === def.name;
Comment on lines +350 to +355
it("invalid: bare .on('error', ...) listener variable is flagged", () => {
cjsRuleTester.run("no-caught-error-interpolation", noCaughtErrorInterpolationRule, {
valid: [],
invalid: [
{
code: `emitter.on('error', err => { log(\`event error: \${err}\`); });`,

@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.

Review: no-caught-error-interpolation EventEmitter extension

This is a clean, well-scoped extension. The implementation is correct and test coverage is thorough.

Key design points verified:

  • isInlineEventErrorHandler mirrors isInlineRejectionHandler with identical guard ordering
  • fnNode.params[0] === def.name reference equality correctly gates flagging to the first (error) parameter only
  • callee.computed guard correctly excludes bracket notation calls — conservative and consistent with existing pattern
  • Named function reference valid case is explicitly tested and correctly excluded (inline-only restriction)
  • All three EventEmitter variants (.on, .once, .addListener) have invalid test cases

No blocking issues found. ✅

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 19.7 AIC · ⌖ 13.8 AIC · ⊞ 5.4K

@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.

Verdict: Request changes

The new EventEmitter detection is a purely syntactic heuristic (method name on/once/addListener + literal "error") with no type information, so it will false-positive on any non-EventEmitter .on('error', fn) call (jQuery, custom pub/sub, etc.), and it silently fails to flag destructured error parameters — both are unaddressed and untested.

Themes
  • False positives: isInlineEventErrorHandler can't distinguish Node EventEmitter from unrelated .on('error', ...) APIs where the callback argument isn't an Error. This should at minimum be documented as an accepted tradeoff with a covering test, or narrowed with type info.
  • False negatives: fnNode.params[0] === def.name doesn't handle destructured first parameters (.on('error', ({message}) => ...)), which silently fall through unflagged with no test to confirm this is intentional.
  • Test additions themselves are solid and cover the main .on/.once/.addListener + named-function-exclusion cases well.

🔎 Code quality review by PR Code Quality Reviewer · auto · 23.3 AIC · ⌖ 4.69 AIC · ⊞ 7.9K
Comment /review to run again

const callee = parent.callee;
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) return false;
const prop = callee.property;
if (prop.type !== AST_NODE_TYPES.Identifier) return false;

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 heuristic matches any object's .on/.once/.addListener('error', fn) call, not just Node EventEmitters, which will produce false positives on non-Error-passing APIs.

💡 Why this matters and how to narrow it

isInlineEventErrorHandler only checks the method name and that the first argument is the string literal "error" — it has no way to distinguish a real Node EventEmitter from jQuery-style .on('error', fn) (jQuery passes a jQuery.Event, not an Error), RxJS-like observables, or arbitrary user-defined pub/sub classes with an on method. Unlike .catch/.then, which are effectively unambiguous promise-shaped names, on/once/addListener are extremely common generic method names across unrelated APIs.

Concretely, this will now force String(err)/getErrorMessage(err) wrapping on code like:

$(elem).on('error', (event) => log(`img failed: ${event}`)); // event is not an Error

Consider either:

  1. Restricting to call sites where the receiver is a known EventEmitter-typed variable (requires type information via ESLintUtils.RuleCreator + parserServices), or
  2. At minimum documenting this as a known limitation/tradeoff in the rule's docs.description, and adding a test case demonstrating the accepted false-positive behavior so it's an intentional, reviewed tradeoff rather than an unverified assumption.

if (isInlineRejectionHandler(fnNode)) return true;
if (isInlineEventErrorHandler(fnNode)) {
// Only the first parameter receives the error object from an 'error' event
return fnNode.params[0] === def.name;

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.

fnNode.params[0] === def.name silently fails to flag destructured error parameters, e.g. .on('error', ({message}) => ...), leaving that case untested and its behavior unverified.

💡 Why this matters

When the first parameter is a destructuring pattern (ObjectPattern/ArrayPattern), def.name refers to the Identifier bound inside the pattern, not the pattern node itself, so it never strictly equals fnNode.params[0]. The function returns false in that branch, meaning destructured error bindings from an .on('error', ...) handler are never flagged — a silent false negative rather than a crash, but it's an edge case with no test coverage, so there's no evidence this fallthrough is the intended behavior versus an overlooked gap.

Add a test case such as:

emitter.on('error', ({message}) => log(`err: ${message}`));

to confirm/document whether destructured members should be in scope for this rule, and to guard against regressions.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 90/100 — Excellent

Analyzed 5 test(s): 5 design, 0 implementation, 0 violation(s).

📊 Metrics (5 tests)
Metric Value
Analyzed 5 (Go: 0, JS/TS: 5)
✅ Design 5 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 5 (100%)
Duplicate clusters 0
Inflation ⚠️ Yes (test: ~106 lines, prod: ~28 lines ≈ 3.8:1)
🚨 Violations 0
Test File Classification Issues
valid: non-'error' EventEmitter event name is not flagged no-caught-error-interpolation.test.ts design_test / behavioral_contract / high_value None
valid: named/hoisted listener function passed to .on('error', ...) is not flagged no-caught-error-interpolation.test.ts design_test / behavioral_contract / high_value None
invalid: bare .on('error', ...) listener variable is flagged no-caught-error-interpolation.test.ts design_test / behavioral_contract / high_value None
invalid: bare .once('error', ...) listener variable is flagged no-caught-error-interpolation.test.ts design_test / behavioral_contract / high_value None
invalid: bare .addListener('error', ...) listener variable is flagged no-caught-error-interpolation.test.ts design_test / behavioral_contract / high_value None
⚠️ Flagged Tests (1 — inflation notice only)

Test inflation (no-caught-error-interpolation.test.ts) — 106 new test lines vs. 28 new production lines ≈ 3.8:1, exceeding the 2:1 guideline.

This is not a quality failure; the ratio reflects thorough ESLint RuleTester cases (valid inputs, invalid inputs, per-suggestion output assertions). Each test case carries genuine behavioral weight verifying both positive and negative rule behavior. No remediation needed.

Verdict

passed. 0% implementation tests (threshold: 30%). Five new EventEmitter .on/.once/.addListener 'error' handler tests covering valid boundary cases (non-error event names, named function references) and three invalid cases with String(err) suggestion output assertions. Inflation noted but does not fail the score.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 · 43.3 AIC · ⌖ 8.25 AIC · ⊞ 8.5K ·
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.

Skills-Based Review 🧠

Applied /tdd and /diagnosing-bugs — changes look correct, with two minor test-coverage gaps flagged inline.

📋 Key Themes & Highlights

Key Themes

  • Missing edge-case test: The fnNode.params[0] === def.name guard (suppressing over-flagging of extra handler params) has no test to protect it from future regressions.
  • Missing FunctionExpression test: The implementation covers both ArrowFunctionExpression and FunctionExpression, but only arrow-function cases appear in the new tests.

Positive Highlights

  • ✅ Clean structural mirror of the existing isInlineRejectionHandler — consistent, navigable pattern
  • ✅ Grounded repro from mcp_server_core.cjs:1052 included as a test fixture — excellent practice
  • ✅ Valid-case tests (non-error events, hoisted function refs) explicitly document the intended scope boundaries
  • callee.computed check correctly excludes emitter['on']('error', ...) dynamic-property forms
  • ✅ Docs description updated to list the new scope

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 29.5 AIC · ⌖ 12 AIC · ⊞ 7.1K
Comment /matt to run again

// Only the first parameter receives the error object from an 'error' event
return fnNode.params[0] === def.name;
}
return false;

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 fnNode.params[0] === def.name guard (lines 75–78) suppresses false-positives on extra parameters, but there is no test verifying this branch holds.

💡 Suggested test

Add a valid case to confirm that extra params in an error handler are not flagged:

it("valid: second param in .on('error', ...) is not flagged", () => {
  cjsRuleTester.run("no-caught-error-interpolation", noCaughtErrorInterpolationRule, {
    valid: [
      `emitter.on('error', (err, context) => { log(\`ctx: \${context}\`); });`,
    ],
    invalid: [],
  });
});

Without this, a future refactor could silently drop the guard and start over-flagging context.

@copilot please address this.

});
});

it("valid: non-'error' EventEmitter event name is not flagged", () => {

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 test for the named/hoisted function reference passes only because the rule never sees the inner template literal (it's inside a separate function declaration, not inside the .on callback). Consider adding a case where a FunctionExpression (not arrow) is used inline to confirm both callback forms are covered:

💡 Suggested test for inline FunctionExpression
it("invalid: inline function expression in .on('error', ...) is flagged", () => {
  cjsRuleTester.run("no-caught-error-interpolation", noCaughtErrorInterpolationRule, {
    valid: [],
    invalid: [
      {
        code: `emitter.on('error', function(err) { log(\`error: \${err}\`); });`,
        errors: [{ messageId: "bareErrorInterpolation", data: { errorVar: "err" } }],
      },
    ],
  });
});

The implementation handles FunctionExpression via the union type, but no test exercises it.

@copilot please address this.

@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: 90/100. 0% implementation tests (threshold: 30%).

@pelikhan
pelikhan merged commit 3ee4296 into main Aug 2, 2026
42 checks passed
@pelikhan
pelikhan deleted the copilot/eslint-factory-fix-no-caught-error-interpolation branch August 2, 2026 13:12
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.84.3

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

eslint-factory: no-caught-error-interpolation misses EventEmitter-style .on('error', err => ...) handler parameters

3 participants