Skip to content

[eslint-miner] eslint-factory: add no-string-fallback-for-non-string-message rule - #55052

Merged
pelikhan merged 1 commit into
mainfrom
eslint-miner/no-string-fallback-for-non-string-message-449359239243a6f0
Aug 23, 2026
Merged

pelikhan merged 1 commit into
mainfrom
eslint-miner/no-string-fallback-for-non-string-message-449359239243a6f0

Conversation

@github-actions

@github-actions github-actions Bot commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

test minimal> Generated by PR Description Updater for #55052 · auto · 28 AIC · ⌖ 4.09 AIC · ⊞ 7.7K · ◷

Detects the pattern:
  typeof <chain>.message === "string" ? <chain>.message : String(<container>)
where the String() fallback stringifies a different (container) expression
than the .message chain under test. When .message exists but is non-string,
this silently produces "[object Object]" instead of coercing the message
value itself.

This mirrors the real bug reported in issue #55014 (error_helpers.cjs
getErrorMessage()) and the rule additionally flags 4 live occurrences of
the same anti-pattern in actions/setup/js: dispatch_workflow.cjs,
log_parser_shared.cjs, route_slash_command.cjs, and safeoutputs_cli.cjs.

Registered at warn severity in eslint.config.cjs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor Author

Thank you for the contribution! This is a well-structured eslint rule with solid test coverage and clear documentation. However, there's an important process issue:

🚫 This PR violates the contribution process — According to our CONTRIBUTING.md, traditional pull requests from non-core team members are not enabled. The project uses agentic development primarily via Copilot coding agents.

Instead of opening a PR directly, please:

  1. Create a detailed GitHub issue describing the bug (the four flagged locations with structural code pattern) and the proposed rule design.
  2. Discuss the approach with the core team in the issue.
  3. A core team member will then create and implement the PR using agents.

This process ensures alignment with the project's agentic workflow and helps the team prioritize contributions effectively.

Note: The PR content itself looks solid — the rule is well-designed, tests are comprehensive, and the scope is clear. Once you open an issue, the core team can take it from there!

Generated by ✅ Contribution Check · auto · 49.8 AIC · ⌖ 6.74 AIC · ⊞ 9.3K · ◷

@pelikhan
pelikhan marked this pull request as ready for review August 23, 2026 13:47
Copilot AI balanced review requested due to automatic review settings August 23, 2026 13:47
@github-actions

github-actions Bot commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor Author

🧠 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 23, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ Test Quality Sentinel completed test quality analysis.

No test files were added or modified in this PR. Test Quality Sentinel skipped.

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ Ponytail Reviewer completed successfully!

Lean already. Ship.

Generated by Ponytail Reviewer for #55052

@github-actions

github-actions Bot commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor Author

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

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

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

Copy link
Copy Markdown
Contributor Author

Comment Memory

reviewed_at: 2026-08-23T13:49:00Z
review_event: REQUEST_CHANGES
top_themes:
  - matcher misses reordered && guards
  - regression test coverage too narrow for equivalent AST shapes
files_reviewed:
  - eslint-factory/eslint.config.cjs
  - eslint-factory/src/index.ts
  - eslint-factory/src/rules/no-string-fallback-for-non-string-message.test.ts
  - eslint-factory/src/rules/no-string-fallback-for-non-string-message.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 · 8.22 AIC · ⌖ 8.83 AIC · ⊞ 7K · ◷
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.

Request changes

The new rule has a real matcher hole: it only recognizes one specific && operand ordering, so a trivial guard rewrite makes the bug pattern invisible again.

Details

The implementation only descends into node.right for logical && tests. That means the rule misses equivalent conditionals where typeof <chain>.message === "string" is not the rightmost operand, and the current tests do not cover that case. Please make the matcher walk the whole && tree and add a regression test for a reordered guard.

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 8.22 AIC · ⌖ 8.83 AIC · ⊞ 7K
Comment /review to run again

// Support `<guard> && typeof <chain>.message === "string"` by checking the
// rightmost operand of a right-associated `&&` chain.
if (node.type === AST_NODE_TYPES.LogicalExpression && node.operator === "&&") {
return getTypeofMessageCheckChainKey(node.right);

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.

This matcher only inspects the right-hand side of an && chain, so it misses the same bug shape when the typeof x.message === "string" check is followed by another guard (for example typeof err.message === "string" && err.message.length > 0 ? ... : String(err)). That makes the rule fragile against trivial reordering and undermines the claim that it prevents this recurring pattern.

💡 Why this matters and how to fix it

Right now getTypeofMessageCheckChainKey() does:

if (node.type === AST_NODE_TYPES.LogicalExpression && node.operator === "&&") {
  return getTypeofMessageCheckChainKey(node.right);
}

So it only works when the .message typeof check is the rightmost operand. As soon as a developer writes another guard on the right, the rule silently stops matching even though the conditional still has the same semantics.

Please walk both sides of the && tree and return the first matching typeof <chain>.message === "string" predicate instead of assuming a specific operand order. Add a regression test for a shape like:

const m = typeof err.message === "string" && err.message.length > 0
  ? err.message
  : String(err);

That keeps the rule resilient to common guard rewrites instead of only one AST layout.

@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-string-fallback-for-non-string-message ESLint rule

The implementation is clean, well-tested, and correctly targets the described anti-pattern.

Highlights:

  • memberChainKey correctly handles ChainExpression wrappers (optional chaining) and bails out on computed properties — avoiding false positives.
  • getTypeofMessageCheckChainKey correctly traverses &&-guarded conditions (e.g. typeof a.b === "object" && typeof a.b.message === "string") by recursing into the right-hand operand.
  • The final alternateChainKey === messageChainKey guard is the right gate to avoid false positives when the fallback already coerces the message itself.
  • Test cases cover the key valid/invalid scenarios including nested chains, unrelated checks, and the &&-guard form.

No blocking issues found.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 33.5 AIC · ⌖ 9.04 AIC · ⊞ 6.2K

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 to detect unsafe error-message fallback patterns in setup JavaScript.

Changes:

  • Implements and registers the new rule.
  • Enables it at warning severity.
  • Adds valid and invalid unit cases.
Show a summary per file
File Description
eslint-factory/src/rules/no-string-fallback-for-non-string-message.ts Implements detection and diagnostics.
eslint-factory/src/rules/no-string-fallback-for-non-string-message.test.ts Tests rule behavior.
eslint-factory/src/index.ts Registers the rule.
eslint-factory/eslint.config.cjs Enables the rule.

Review details

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

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

schema: [],
messages: {
stringifiesContainerInsteadOfMessage:
"This falls back to String({{containerExpr}}) instead of coercing {{messageChain}} itself when it exists but isn't a string — risks producing \"[object Object]\". Use String({{messageChain}}) instead.",
Comment on lines +86 to +88
// Only report when String() is applied to a *different* expression
// than the `.message` chain (i.e. stringifying the container object).
if (alternateChainKey === messageChainKey) return;
Comment on lines +62 to +66
docs: {
description:
"Disallow `typeof <x>.message === \"string\" ? <x>.message : String(<container>)` where the String() fallback stringifies a different (container) " +
"expression than the `.message` chain being tested. When `.message` exists but isn't a string, this pattern silently produces `[object Object]` " +
"instead of coercing the message value itself (e.g. `String(<x>.message)`). Mirrors the real bug found in error_helpers.cjs's getErrorMessage().",
@github-actions github-actions Bot mentioned this pull request Aug 23, 2026

@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 /codebase-design — commenting with improvement suggestions; no blocking correctness issues.

📋 Key Themes & Highlights

Key Themes

  • Test coverage gaps on the &&-guard path: the valid suite doesn't exercise the &&-unwrapping code path for accepted inputs, leaving a regression window.
  • Missing hasSuggestions: the rule knows the exact fix but doesn't expose it; adding it would save every developer who hits this warning from manually deriving the same substitution.
  • meta.type / severity mismatch: type: "problem" paired with "warn" severity conflicts with ESLint convention; "suggestion" is more accurate if "warn" is the permanent severity.

Positive Highlights

  • ✅ Well-scoped, low false-positive rule design — fires only when the String() argument is structurally distinct from the tested .message chain.
  • ✅ Good use of structural key comparison (memberChainKey) to avoid source-text equality pitfalls.
  • ✅ Comprehensive invalid test cases mirroring real occurrences in the codebase.
  • ✅ Clean integration into plugin exports and eslint.config.cjs.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 43.9 AIC · ⌖ 11.5 AIC · ⊞ 7.6K
Comment /matt to run again

valid: [
// Fallback coerces the message itself, not the container.
`const m = typeof err.message === "string" ? err.message : String(err.message);`,
// Nested chain, still coerces the same chain.

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 &&-guarded path in getTypeofMessageCheckChainKey is exercised only by the third invalid case — the valid suite lacks a matching case with a correct fallback (e.g. typeof x === "object" && typeof x.message === "string" ? x.message : String(x.message)). Without it, the positive arm of the && branch is untested: a regression that accidentally dropped the && unwrapping would still pass the existing tests.

💡 Suggested test addition

Add to the valid array:

// Guarded by leading object-type check, fallback coerces the message itself.
`const m = typeof err === "object" && typeof err.message === "string" ? err.message : String(err.message);`,
// Deeply nested with leading guard, still correct fallback.
`const m = typeof err.response === "object" && typeof err.response.data.message === "string" ? err.response.data.message : String(err.response.data.message);`,

This ensures the &&-unwrapping code path is exercised for accepted inputs too, not just flagged ones.

@copilot please address this.

docs: {
description:
"Disallow `typeof <x>.message === \"string\" ? <x>.message : String(<container>)` where the String() fallback stringifies a different (container) " +
"expression than the `.message` chain being tested. When `.message` exists but isn't a string, this pattern silently produces `[object Object]` " +

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.

[/codebase-design] The rule can provide a deterministic fix since the correct substitution is always String(<messageChainKey>), but meta declares neither fixable nor hasSuggestions. Developers hitting this warning must manually derive the fix at each site.

💡 Suggested improvement: add `hasSuggestions`
meta: {
  type: "problem",
  hasSuggestions: true,
  // ...
  messages: {
    stringifiesContainerInsteadOfMessage: "...",
    replaceWithMessageChain:
      "Replace {{containerExpr}} with {{messageChain}} to coerce the message value instead.",
  },
},

And in context.report:

context.report({
  node: node.alternate,
  messageId: "stringifiesContainerInsteadOfMessage",
  data: { containerExpr, messageChain: messageChainKey },
  suggest: [
    {
      messageId: "replaceWithMessageChain",
      data: { containerExpr, messageChain: messageChainKey },
      fix(fixer) {
        return fixer.replaceText(node.alternate, `String(${messageChainKey})`);
      },
    },
  ],
});

Since the correct substitution is deterministic, a suggestion is low risk and avoids each developer having to re-derive the same fix.

@copilot please address this.

export const noStringFallbackForNonStringMessageRule = createRule({
name: "no-string-fallback-for-non-string-message",
meta: {
type: "problem",

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 meta.type is set to "problem", which signals that violations are errors or likely bugs — yet the rule is wired at "warn" severity. This mismatch is low-risk but can confuse tooling and contributors: "problem" implies the rule should eventually be "error". If "warn" is the long-term intent (case-by-case decision), "suggestion" is the more accurate meta.type.

💡 Suggested fix

Change to one of:

// If warn is the permanent severity (per-site decision required):
type: "suggestion",

// If warn is temporary until all sites are fixed and rule moves to "error":
// keep type: "problem" but document the promotion plan in a comment or PR

Other rules in this factory that are registered at warn use "suggestion" as their meta.type, which is consistent.

@copilot please address this.

`const m = typeof err.message === "string" ? err.message : err.toString();`,
],
invalid: [],
});

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 valid suite has no test for the case where node.arguments.length !== 1 in getStringCallArgChainKey — e.g. String() or String(a, b). These are accepted today (returns null → no report), but there is no test confirming that behaviour. If someone changes the length guard, the test suite won't catch it.

💡 Suggested tests to add
// String() with no arguments — should not fire.
`const m = typeof err.message === "string" ? err.message : String();`,
// String() with multiple arguments (unusual, but syntactically valid).
`const m = typeof err.message === "string" ? err.message : String(err, "extra");`,

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

pr-sous-chef
@copilot this PR was triaged and could use a maintainer-ready refresh.

Please refresh the branch with the latest base changes if GitHub offers it, address any remaining review feedback or check noise, and then run the pr-finisher skill.

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

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 11.3 AIC · ⌖ 8.24 AIC · ⊞ 7.1K · ◷
Comment /souschef to run again

@pelikhan
pelikhan merged commit c4bb131 into main Aug 23, 2026
55 checks passed
@pelikhan
pelikhan deleted the eslint-miner/no-string-fallback-for-non-string-message-449359239243a6f0 branch August 23, 2026 14:39
Copilot stopped work on behalf of gh-aw-bot due to an error August 23, 2026 14:39
@github-actions

Copy link
Copy Markdown
Contributor Author

🎉 This pull request is included in a new release.

Release: v0.87.5

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.

3 participants