Skip to content

[eslint-miner] eslint: add no-err-stack-then-string-fallback rule#47541

Merged
pelikhan merged 3 commits into
mainfrom
eslint-no-err-stack-then-string-fallback-ef1fdbb74f7adc8e
Jul 23, 2026
Merged

[eslint-miner] eslint: add no-err-stack-then-string-fallback rule#47541
pelikhan merged 3 commits into
mainfrom
eslint-no-err-stack-then-string-fallback-ef1fdbb74f7adc8e

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new ESLint rule no-err-stack-then-string-fallback to the gh-aw-custom plugin. The rule flags the antipattern err && err.stack ? err.stack : String(err) and suggests replacing it with getErrorMessage(err) from error_helpers.cjs.

What changed

File Change
eslint-factory/src/rules/no-err-stack-then-string-fallback.ts New rule implementation using @typescript-eslint/utils; type suggestion; provides an auto-fix suggestion
eslint-factory/src/rules/no-err-stack-then-string-fallback.test.ts Test suite: 7 valid boundary/exclusion cases and 3 invalid cases (core.setFailed, standalone assignment, alternate variable name)
eslint-factory/src/index.ts Imports and registers the new rule in the plugin
eslint-factory/eslint.config.cjs Enables the rule at warn severity

Rule behaviour

  • Triggers on: ConditionalExpression matching <errVar> && <errVar>.stack ? <errVar>.stack : String(<errVar>)
  • Does not trigger on: instanceof form, logical-OR form (err.stack || String(err)), mismatched variable names, non-stack properties
  • Suggestion fix: replaces the entire ternary with getErrorMessage(<errVar>) — caller must ensure the import is present
  • Message template: uses {{errorVar}} placeholder for variable-specific diagnostics

Motivation

The err.stack ternary surfaces full stack frames in contexts that need only a concise error message. getErrorMessage() from error_helpers.cjs is already available in every actions/setup/js script and returns a clean, consistent string.

Commits

  • f50672e eslint: add no-err-stack-then-string-fallback rule
  • a02378dcd eslint: shorten verbose message and add boundary tests for no-err-stack rule
  • 1a70da0ca eslint: use {{errorVar}} placeholder in err.stack ternary message description

Generated by PR Description Updater for #47541 · sonnet46 48.3 AIC · ⌖ 7.48 AIC · ⊞ 4.8K ·

Flag the recurring pattern `err && err.stack ? err.stack : String(err)`
and suggest replacing it with `getErrorMessage(err)` from error_helpers.cjs.

The stack-trace form leaks implementation details as the failure message
passed to core.setFailed() or logged directly. Five sites in
actions/setup/js use this pattern today:
  - apply_samples.cjs
  - merge_remote_agent_github_folder.cjs
  - parse_mcp_gateway_log.cjs
  - parse_mcp_scripts_logs.cjs
  - parse_token_usage.cjs

getErrorMessage() is already the established safe accessor for caught
errors in this codebase; this rule closes the gap left by the existing
prefer-get-error-message rule (which targets only the instanceof form).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added automation cookie Issue Monster Loves Cookies! eslint labels Jul 23, 2026
@pelikhan
pelikhan marked this pull request as ready for review July 23, 2026 09:46
Copilot AI review requested due to automatic review settings July 23, 2026 09:46
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

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

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

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

No ADR enforcement needed: PR #47541 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Test Quality Sentinel completed test quality analysis.

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

Adds an ESLint rule that recommends getErrorMessage() instead of stack-heavy error formatting.

Changes:

  • Detects the targeted conditional-expression pattern.
  • Adds suggestion-focused tests and plugin registration.
  • Enables the rule as a warning.
Show a summary per file
File Description
eslint-factory/src/rules/no-err-stack-then-string-fallback.ts Implements the rule and replacement suggestion.
eslint-factory/src/rules/no-err-stack-then-string-fallback.test.ts Tests matching, exclusions, and suggestions.
eslint-factory/src/index.ts Registers the rule.
eslint-factory/eslint.config.cjs Enables the rule as a warning.

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: Medium

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review: no-err-stack-then-string-fallback ESLint rule

The implementation is correct and well-structured.

Strengths:

  • AST matching is precise — all 3 sub-checks verify variable name consistency, preventing false positives on mismatched identifiers.
  • Correctly uses hasSuggestions: true with a safe suggestion (not an auto-fix), appropriate since getErrorMessage import is required.
  • Test coverage is solid: valid cases cover the existing instanceof form, mismatched names, and non-stack properties; invalid cases cover the primary and aliased-variable forms.
  • Registration in index.ts and eslint.config.cjs is consistent with other rules.

No blocking issues found. ✅

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 18.5 AIC · ⌖ 7.96 AIC · ⊞ 5K

@github-actions github-actions Bot mentioned this pull request Jul 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor Author

🧪 Test Quality Sentinel Report

Test Quality Score: 91/100 — Excellent

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

📊 Metrics (7 tests)
Metric Value
Analyzed 7 (TypeScript/Vitest: 7)
✅ Design 7 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 5 (71%)
Duplicate clusters 0
Test inflation 1.23:1 (no)
🚨 Violations 0

Test Coverage:

Test File Classification Coverage
already uses getErrorMessage no-err-stack-then-string-fallback.test.ts design_test Positive case: rule should not flag getErrorMessage()
instanceof form handled no-err-stack-then-string-fallback.test.ts design_test Edge case: instanceof pattern excluded by design
mismatched variable names excluded no-err-stack-then-string-fallback.test.ts design_test Edge case: different variables not flagged
different property than stack excluded no-err-stack-then-string-fallback.test.ts design_test Edge case: .message property (not .stack) excluded
core.setFailed() flagged no-err-stack-then-string-fallback.test.ts design_test Error case: anti-pattern detected + suggestions verified
standalone assignment flagged no-err-stack-then-string-fallback.test.ts design_test Error case: anti-pattern in assignment detected
different variable name (error) flagged no-err-stack-then-string-fallback.test.ts design_test Generalization: works with any error variable name

Verdict

Passed. 0% implementation tests (threshold: 30%), 0 violations.

Strengths:

  • All 7 tests are design tests verifying rule behavior contracts
  • Strong edge-case coverage: variable name variations, property types, boundary conditions
  • Balanced test suite: 4 positive cases (rule shouldn't trigger), 3 negative cases (rule should detect + suggest fixes)
  • ESLint RuleTester provides robust assertion framework (error counts, message IDs, auto-fix suggestions)
  • No test inflation: test:prod ratio 1.23:1 is healthy
  • Clean architectural coverage of the rule's AST pattern matching logic

🧪 Test quality analysis by Test Quality Sentinel · haiku45 12.3 AIC · ⌖ 9.4 AIC · ⊞ 7.1K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 91/100. 0% implementation tests (threshold: 30%). All tests verify design contracts with strong edge-case coverage.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

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 /grill-with-docs — commenting with a few targeted improvements.

📋 Key Themes & Highlights

Issues Found

  1. Missing import in suggestion fix (correctness): Applying the auto-suggestion produces broken code if getErrorMessage is not already required in the target file. The fix body needs to conditionally insert the require call, or the suggestion message must more clearly warn users to do it manually.

  2. Missing boundary test (test coverage): The "mismatched variable names" test exercises the String(other) mismatch but not err && other.stack ? err.stack : String(err) — which is the exact boundary guarded by isErrAndErrStack's object check. Worth adding.

  3. Verbose lint message (UX): The preferGetErrorMessage message repeats {{errorVar}} three times and embeds the full pattern; this is hard to scan in editor popups and CI logs.

Positive Highlights

  • ✅ Excellent decomposition into small, focused helpers (isErrAndErrStack, isErrStack, isStringErr) — easy to read and test
  • ✅ Tests cover both the suggested fix output and multiple variable name forms
  • ✅ PR description includes real-world evidence (5 detected sites) and validation results
  • ✅ Correctly scoped as a complement to the existing prefer-get-error-message rule

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 40.7 AIC · ⌖ 4.95 AIC · ⊞ 6.7K
Comment /matt to run again

context.report({
node,
messageId: "preferGetErrorMessage",
data: { errorVar: errVar },

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/tdd] The suggestion fix replaces the node with getErrorMessage(errVar) but does not insert the require — applying this auto-fix will produce broken code in any file that does not already import getErrorMessage.

💡 Suggested fix

Either (a) extend the fixer to also insert the require when absent, or (b) keep it suggestions-only and update the suggestion message to be clearer that the import must be added manually.

Option (a) sketch:

fix(fixer) {
  const fixes = [fixer.replaceText(node, `getErrorMessage(${errVar})`)];
  const sourceCode = context.getSourceCode();
  const scope = sourceCode.scopeManager?.globalScope;
  const alreadyDefined = scope?.variables.some(v => v.name === "getErrorMessage");
  if (!alreadyDefined) {
    const firstToken = sourceCode.ast.body[0];
    fixes.unshift(fixer.insertTextBefore(firstToken, `const { getErrorMessage } = require("./error_helpers.cjs");\n`));
  }
  return fixes;
}

@copilot please address this.

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 existing prefer-get-error-message rule uses the identical suggestion pattern — replacing the node text without inserting an import — and that rule's message already says "ensure getErrorMessage is imported ... before applying." This is the established codebase convention for suggestion-only fixers in this repo.

ESLint suggestions (unlike auto-fixes) require explicit user opt-in; IDEs do not apply them via "Fix All". The warning in the suggestion message is the codebase's chosen approach to communicate the manual import step. Keeping this rule consistent with the existing pattern.

});
});

it("valid: mismatched variable names are excluded", () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/tdd] The test 'valid: mismatched variable names are excluded' passes err && err.stack ? err.stack : String(other) — but this is excluded because the rule checks that all three errVar references are the same identifier. There is no test for a subtler mismatch: err && other.stack ? err.stack : String(err). This is the boundary that actually validates isErrAndErrStack correctly enforces the left-hand side object of .stack.

💡 Suggested additional valid test
it("valid: mismatched object in err.stack check is excluded", () => {
  cjsRuleTester.run("no-err-stack-then-string-fallback", noErrStackThenStringFallbackRule, {
    valid: [`const msg = err && other.stack ? err.stack : String(err);`],
    invalid: [],
  });
});

@copilot please address this.

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.

Fixed in commit eslint: shorten verbose message and add boundary tests for no-err-stack rule. Added a new valid-case test:

it("valid: mismatched object in err.stack check is excluded", () => {
  cjsRuleTester.run("no-err-stack-then-string-fallback", noErrStackThenStringFallbackRule, {
    valid: [`const msg = err && other.stack ? err.stack : String(err);`],
    invalid: [],
  });
});

This now explicitly covers the isErrAndErrStack object check boundary.

},
defaultOptions: [],
create(context) {
return {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/grill-with-docs] The preferGetErrorMessage message interpolates {{errorVar}} three times inline — making it a very long single-line lint warning. Most editors and CI logs truncate or wrap this. Consider separating the rationale from the identifier so the error is scannable.

💡 Suggested shorter message
preferGetErrorMessage:
  "Prefer getErrorMessage({{errorVar}}) over `{{errorVar}} && {{errorVar}}.stack ? {{errorVar}}.stack : String({{errorVar}})` — stack leaks noisy internals.",

Or split into a primary message + fix suggestion body.

@copilot please address this.

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.

Fixed in commit eslint: shorten verbose message and add boundary tests for no-err-stack rule. The preferGetErrorMessage message now uses {{errorVar}} once and matches the concise style of the existing prefer-get-error-message rule:

"Prefer getErrorMessage({{errorVar}}) from error_helpers.cjs. The `err.stack` ternary surfaces noisy stack frames; getErrorMessage() returns a clean, consistent message."

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changes requested — 1 high issue must be fixed before merge

The rule logic and intent are sound, and the targeted pattern is a real code smell worth enforcing. However, three issues need addressing:

Must fix:

  • Unsafe suggestion fixer (line 58): applying the suggestion without a pre-existing import will produce a ReferenceError at runtime. IDEs that auto-apply ESLint suggestions will silently break affected files. Either insert the import as part of the fix or remove the fix callback and leave it as a manual hint.

Should fix before shipping widely:

  • No scope analysis (line 44): errVar is matched by name only, not by binding. A shadowed variable with the same name in an inner scope triggers a false positive. Use context.getScope() to verify all four identifiers resolve to the same declaration.
  • Plain RuleTester instead of @typescript-eslint/rule-tester (test line 42): locks out any future type-aware guards from being tested.

Minor (non-blocking):

  • Missing valid-case test for mismatched consequent (other.stack), and no test documenting the ||\ form is intentionally out of scope.

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 58.3 AIC · ⌖ 5.1 AIC · ⊞ 5.7K
Comment /review to run again

"Replace with getErrorMessage({{errorVar}}) — ensure getErrorMessage is imported from error_helpers.cjs before applying.",
},
},
defaultOptions: [],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Unsafe suggestion fixer: inserts bare getErrorMessage without verifying it is in scope — applying the suggestion when the import is missing introduces a ReferenceError at runtime.

💡 Details and fix

The suggestion message itself warns "ensure getErrorMessage is imported ... before applying", which is an admission that the fixer is unsafe. IDEs such as VS Code with "Fix All" will auto-apply the suggestion and silently break code.

Two safe options:

Option A — insert the import as part of the fix:

fix(fixer) {
  const sourceCode = context.getSourceCode();
  const firstToken = sourceCode.ast.body[0];
  const alreadyImported = sourceCode.ast.body.some(
    n => n.type === "VariableDeclaration" && sourceCode.getText(n).includes("getErrorMessage")
  );
  const fixes = [fixer.replaceText(node, `getErrorMessage(${errVar})`)];
  if (!alreadyImported)
    fixes.push(fixer.insertTextBefore(firstToken, 'const { getErrorMessage } = require("./error_helpers.cjs");
'));
  return fixes;
}

Option B — omit the fix callback entirely, leaving it as a hint only (the engineer must add the import manually, but at least no auto-broken code).

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 existing prefer-get-error-message rule uses the same suggestion pattern (replaces node text, no import insertion). This is the established codebase convention. ESLint suggestions require explicit user opt-in, so there is no risk of an IDE silently applying them via "Fix All". The suggestion message already warns users to add the import before applying. Keeping consistent with the existing rule.

type: "suggestion",
hasSuggestions: true,
docs: {
description:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No scope analysis: same variable name is not the same binding — the rule can produce false positives when an inner scope shadows the error variable name.

💡 Details

The rule extracts errVar from test.left.name and checks that the same string appears in the test, consequent, and alternate. But identifier names are not bindings. Consider:

function outer(err) {
  return function inner() {
    const err = computeSomethingElse(); // different err
    return err && err.stack ? err.stack : String(err); // WRONG flag
  };
}

Here the inner err is a plain object and flagging it is a false positive. Use context.getScope() (or sourceCode.getScope(node) in ESLint v9) to resolve each identifier to its declaration and verify they are the same binding before reporting.

This is less critical at the current five call-sites (which all use the catch-bound err), but the rule is registered globally and will run on all consumer code.

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 existing prefer-get-error-message rule uses the same name-based matching without scope analysis. Scope analysis would be a separate enhancement applicable to both rules. The practical false-positive risk is low since the targeted pattern (err && err.stack ? err.stack : String(err)) almost exclusively appears in catch blocks where err is the catch binding — a distinct declaration context. Keeping consistent with the existing rule's approach.

});

it("invalid: core.setFailed(err && err.stack ? err.stack : String(err)) is flagged", () => {
cjsRuleTester.run("no-err-stack-then-string-fallback", noErrStackThenStringFallbackRule, {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Test harness uses base ESLint RuleTester, not @typescript-eslint/rule-tester — type-aware scenarios cannot be tested with the current setup.

💡 Details

This rule is created with ESLintUtils.RuleCreator from @typescript-eslint/utils, which means it can optionally use TypeScript type information via ESLintUtils.getParserServices(context). But the test harness uses RuleTester from plain eslint with no parserOptions.project, so:

  1. Any type-aware guard added in the future (e.g., "only flag when the variable is typed unknown or Error") cannot be tested here.
  2. It also means the current rule fires regardless of the TypeScript type of errVar. If a future maintainer adds type-checking to reduce false positives, the test suite won't cover it.

Switch to:

import { RuleTester } from "`@typescript-eslint/rule-tester`";

and supply a tsconfig via RuleTester.afterAll / languageOptions.parserOptions.project to unlock type-aware test cases.

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.

Every existing rule in this codebase uses RuleTester from plain eslint — including prefer-get-error-message, no-json-stringify-error, no-unsafe-catch-error-property, and all others. Switching to @typescript-eslint/rule-tester would diverge from the established convention. This rule is kept consistent with the rest of the codebase.

});
});

it("invalid: standalone assignment is flagged", () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Missing test: mismatched consequent variable is not covered — a future refactor that breaks the consequent guard will go undetected.

💡 Details

There is a valid-case test for a mismatched alternate (String(other) instead of String(err)), but no equivalent test for a mismatched consequent. The guard in question is isErrStack(node.consequent, errVar). Add:

it("valid: mismatched consequent variable is excluded", () => {
  cjsRuleTester.run("no-err-stack-then-string-fallback", noErrStackThenStringFallbackRule, {
    valid: [`const msg = err && err.stack ? other.stack : String(err);`],
    invalid: [],
  });
});

Also missing: a valid case for err.stack || String(err) (logical-OR form, not a ternary) to document that it is intentionally out of scope.

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.

Fixed in commit eslint: shorten verbose message and add boundary tests for no-err-stack rule. Added two new valid-case tests:

  1. Mismatched consequent variable (err && err.stack ? other.stack : String(err)) — directly exercises the isErrStack(node.consequent, errVar) guard.
  2. Logical-OR form (err.stack || String(err)) — documents that the non-ternary variant is intentionally out of scope.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address the outstanding review feedback on this PR, and rerun the relevant checks once the branch is up to date.

Run: https://github.com/github/gh-aw/actions/runs/29998171888

Generated by 👨‍🍳 PR Sous Chef · gpt54 8.92 AIC · ⌖ 7.98 AIC · ⊞ 7K ·
Comment /souschef to run again

Copilot AI and others added 2 commits July 23, 2026 10:34
…ck rule

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
…cription

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Copilot AI requested a review from gh-aw-bot July 23, 2026 10:37
@pelikhan
pelikhan merged commit 30e222d into main Jul 23, 2026
@pelikhan
pelikhan deleted the eslint-no-err-stack-then-string-fallback-ef1fdbb74f7adc8e branch July 23, 2026 10:39
@github-actions

Copy link
Copy Markdown
Contributor Author

🎉 This pull request is included in a new release.

Release: v0.83.1

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

Labels

automation cookie Issue Monster Loves Cookies! eslint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants