Skip to content

[eslint-miner] eslint: add no-core-error-then-setfailed rule - #48354

Merged
pelikhan merged 4 commits into
mainfrom
eslint-miner/no-core-error-then-setfailed-f3ca87d2a9ef0f8d
Jul 27, 2026
Merged

[eslint-miner] eslint: add no-core-error-then-setfailed rule#48354
pelikhan merged 4 commits into
mainfrom
eslint-miner/no-core-error-then-setfailed-f3ca87d2a9ef0f8d

Conversation

@github-actions

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

Copy link
Copy Markdown
Contributor

Caution

agentic threat detected
Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.

Details

Potential security threats were detected in the agent output.

Review the workflow run logs for details.

test body

Generated by PR Description Updater for #48354 · sonnet46 · 51.7 AIC · ⌖ 4.66 AIC · ⊞ 4.8K ·

Add a new custom ESLint rule that flags the redundant pattern of calling
core.error(msg) immediately before core.setFailed(msg). Since core.setFailed()
already logs an error annotation and marks the action as failed, the preceding
core.error() call creates a duplicate annotation in the GitHub Actions log.

The rule detects this in BlockStatement, SwitchCase, and Program bodies.
It provides an auto-fix suggestion to remove the redundant core.error() call.

Evidence: 7 violations found across actions/setup/js/*.cjs in the current
codebase (check_permissions.cjs, unlock-issue.cjs, add_reaction_and_edit_comment.cjs,
add_reaction.cjs, push_repo_memory.cjs, start_mcp_gateway.cjs x3,
validate_memory_files.cjs).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@pelikhan
pelikhan marked this pull request as ready for review July 27, 2026 11:26
Copilot AI review requested due to automatic review settings July 27, 2026 11:26
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

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.

@github-actions

github-actions Bot commented Jul 27, 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.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

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

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 intended to detect redundant core.error() calls before core.setFailed().

Changes:

  • Implements and registers the rule.
  • Enables it as a warning.
  • Adds rule tests and suggestions.
Show a summary per file
File Description
eslint-factory/src/rules/no-core-error-then-setfailed.ts Implements detection and removal suggestions.
eslint-factory/src/rules/no-core-error-then-setfailed.test.ts Adds rule test cases.
eslint-factory/src/index.ts Registers the rule.
eslint-factory/eslint.config.cjs Enables the rule.

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: 3
  • Review effort level: Medium

Comment on lines +88 to +92
const next = stmts[i + 1];
if (!isCoreSetFailedStatement(next, sourceCode)) continue;

const errorCall = current.expression as TSESTree.CallExpression;
const safeToFix = hasSingleNonSpreadArgument(errorCall);
Comment on lines +91 to +92
const errorCall = current.expression as TSESTree.CallExpression;
const safeToFix = hasSingleNonSpreadArgument(errorCall);
@@ -0,0 +1,73 @@
import { RuleTester } from "@typescript-eslint/rule-tester";

@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-core-error-then-setfailed rule

Overall the implementation is clean, well-reasoned, and well-tested. A few minor non-blocking observations:

Non-blocking notes

1. Program node filter is incomplete

The Program handler filters out ImportDeclaration and ExportAllDeclaration but not ExportNamedDeclaration or ExportDefaultDeclaration. In practice this is harmless (those aren't ExpressionStatements), but the filtering intent is inconsistent with itself.

2. Suggestion leaves leading whitespace

fixer.remove(current) removes only the node's text range, not surrounding whitespace — this produces a leading space in the suggestion output (visible in the test: output: core.setFailed(...)``). This is standard ESLint behavior and acceptable for a non-autofix suggestion.

No blocking issues. The rule correctly identifies the duplicate-annotation anti-pattern and the test coverage is solid.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 23.5 AIC · ⌖ 4.56 AIC · ⊞ 5K

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

Three issues to fix before merge

The rule logic is sound and the rationale is correct. Three issues need fixing:

  1. hasSingleNonSpreadArgument suppresses the autofix incorrectly — the fix removes the entire statement, so the guard is always safe to drop.
  2. Program body filter has an incomplete exclusion listExportNamedDeclaration and ExportDefaultDeclaration pass through with an incorrect type assertion.
  3. No same-object check between the two callsc1.error(); c2.setFailed() with different aliases (both in CORE_ALIASES) triggers a false positive.
Other findings (non-blocking)
  • isCoreErrorStatement / isCoreSetFailedStatement are near-identical and should be unified into a single isCoreMethodCallStatement(node, sourceCode, methodName) helper.
  • fixer.remove(current) does not consume the trailing newline, leaving a stray blank line after the autofix is applied. Extend the removal range to cover the trailing .

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

Comments that could not be inline-anchored

eslint-factory/src/rules/no-core-error-then-setfailed.ts:207

hasSingleNonSpreadArgument guard incorrectly suppresses the autofix suggestion: the fix is fixer.remove(current), which removes the entire statement — always safe regardless of argument count or spread usage. The guard silently drops the suggestion for common patterns like core.error(msg, {title: &quot;ctx&quot;}).

<details>
<summary>💡 Suggested fix</summary>

Remove the guard — always provide the suggestion:

context.report({
  node: current,
  messageId: &quot;noCoreErrorThenSetFai…

</details>

<details><summary>eslint-factory/src/rules/no-core-error-then-setfailed.ts:234</summary>

**Program body filter is incomplete  `ExportNamedDeclaration` and `ExportDefaultDeclaration` pass through as `TSESTree.Statement`**: `TSESTree.Program.body` is `(Statement | ModuleDeclaration)[]`. The filter only excludes `ImportDeclaration` and `ExportAllDeclaration`, so `export function foo() { core.error(...); core.setFailed(...); }` and `export default ...` nodes are cast as `Statement` and handed to `checkStatements`. While helpers bail out immediately (they check for `ExpressionStatement…

</details>

<details><summary>eslint-factory/src/rules/no-core-error-then-setfailed.ts:146</summary>

**No cross-object validation  different core aliases trigger a false positive**: when two different identifiers are both in `CORE_ALIASES`, the rule reports `c1.error(&quot;x&quot;); c2.setFailed(&quot;x&quot;)` even though `c1` and `c2` are different objects (e.g., two separate `@actions/core` imports or a mock and a real instance). The adjacency check should verify that both statements reference the same identifier name.

&lt;details&gt;
&lt;summary&gt;💡 Suggested fix&lt;/summary&gt;

In `checkStatements`, after confirming both…

</details>

@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 — requesting changes on a few correctness gaps and a maintainability concern.

📋 Key Themes & Highlights

Key Themes

  • DRY violation: isCoreErrorStatement and isCoreSetFailedStatement are near-identical; a shared isCoreMethodCallStatement(node, methodName, sc) helper would make future changes safer.
  • Missing edge-case tests: No test verifies that mixed-receiver pairs (coreA.error(); coreB.setFailed()) or non-core aliases (const c = notCore) are handled correctly.
  • Program visitor scope: The filter for top-level statements silently skips ExportNamedDeclaration / ExportDefaultDeclaration, which may cause inconsistent coverage in module-style files.

Positive Highlights

  • ✅ Rule rationale is solid and well-evidenced (7 real violations found)
  • ✅ Good coverage of computed property access and alias patterns
  • ✅ Auto-fix suggestion is correctly scoped to single-argument calls only
  • ✅ Test suite structure (valid / invalid) is clear and readable

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

if (!isSetFailedNonComputed && !isSetFailedComputed) return false;
if (obj.type !== AST_NODE_TYPES.Identifier) return false;

return isCoreLikeIdentifier(obj.name) || isCoreAliasIdentifier(obj, sourceCode);

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] isCoreErrorStatement and isCoreSetFailedStatement share ~90% identical logic — only the method name differs. This duplication will require parallel edits if the detection logic ever changes.

💡 Suggested refactor

Extract a single generic helper:

function isCoreMethodCallStatement(
  node: TSESTree.Statement,
  methodName: string,
  sourceCode: SourceCode
): node is TSESTree.ExpressionStatement {
  if (node.type !== AST_NODE_TYPES.ExpressionStatement) return false;
  const expr = node.expression;
  if (expr.type !== AST_NODE_TYPES.CallExpression) return false;
  const callee = expr.callee;
  if (callee.type !== AST_NODE_TYPES.MemberExpression) return false;
  const obj = callee.object;
  const prop = callee.property;
  const isTarget =
    (!callee.computed && prop.type === AST_NODE_TYPES.Identifier && prop.name === methodName) ||
    (callee.computed && prop.type === AST_NODE_TYPES.Literal && prop.value === methodName);
  if (!isTarget || obj.type !== AST_NODE_TYPES.Identifier) return false;
  return isCoreLikeIdentifier(obj.name) || isCoreAliasIdentifier(obj, sourceCode);
}

Then the two exported helpers collapse to one-liners:

const isCoreError = (n: TSESTree.Statement, sc: SourceCode) =>
  isCoreMethodCallStatement(n, 'error', sc);
const isCoreSetFailed = (n: TSESTree.Statement, sc: SourceCode) =>
  isCoreMethodCallStatement(n, 'setFailed', sc);

@copilot please address this.

// core.error without adjacent setFailed (setFailed is non-adjacent)
{
code: `core.error("msg"); doSomething(); core.setFailed("msg");`,
},

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] Missing test: core.error and core.setFailed called on different objects (e.g. coreA.error('x'); coreB.setFailed('y')) should be valid — the rule currently doesn't check that both calls share the same receiver.

💡 Suggested test case
// Different receivers — should NOT be flagged
{
  code: `coreA.error("msg"); coreB.setFailed("msg");`,
},

The implementation checks each identifier independently against CORE_ALIASES, so two different aliased receivers would both pass, resulting in a false positive for mixed-receiver pairs.

@copilot please address this.

},
SwitchCase(node: TSESTree.SwitchCase) {
checkStatements(node.consequent);
},

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 Program body filter excludes ImportDeclaration and ExportAllDeclaration but not ExportNamedDeclaration or ExportDefaultDeclaration. Top-level patterns inside exported functions won't be reached via Program, though they will be via BlockStatement. Consider simplifying to just rely on BlockStatement + SwitchCase — the Program visitor only catches the rare case of top-level imperative scripts.

@copilot please address this.

// Computed property access
{
code: `core["error"]("msg"); core["setFailed"]("msg");`,
errors: [{ messageId: "noCoreErrorThenSetFailed" }],

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 alias test (const c = core; c.error...) relies on isCoreAliasIdentifier resolving c to core, but none of the valid-case tests verify that an aliased call to a non-core object is correctly ignored (e.g. const c = notCore; c.error('x'); c.setFailed('y') should be valid). Adding this would guard against CORE_ALIASES over-matching.

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address the unresolved review feedback below, and then reply with whether this PR is ready for maintainer review.

Unresolved review feedback / follow-up (newest first):

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

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

- Only flag when core.error() and core.setFailed() use provably equivalent
  message arguments (same source text); different messages are no longer
  reported since core.error may carry extra diagnostic context
- Exclude core.error() calls with annotation properties (second argument)
  as they provide context not duplicated by setFailed
- Restrict auto-remove suggestion to side-effect-free arguments; function
  call arguments are no longer silently dropped
- Replace @typescript-eslint/rule-tester with RuleTester from eslint plus
  Vitest describe/it, matching the convention of all sibling rules
- Update test cases to reflect the new message-equivalence requirement

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

Copilot AI commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@copilot please run the pr-finisher skill, address the unresolved review feedback below, and then reply with whether this PR is ready for maintainer review.

Unresolved review feedback / follow-up (...

Addressed all three reviewer concerns in commit fix: address review feedback on no-core-error-then-setfailed rule:

  1. Message equivalence check — the rule now only flags when both calls carry provably identical source text for their first argument. core.error("file: " + path); core.setFailed("action failed") is no longer reported.

  2. Annotation propertiescore.error(msg, { title: "..." }) is excluded; the second argument carries diagnostic context not repeated by setFailed.

  3. Side-effect-free suggestion guard — the auto-remove suggestion is only offered when the shared argument is provably side-effect-free (literal, identifier, member expression, template literal with safe expressions, binary/unary expressions of safe nodes). For call-expression arguments like nextMessage(), the violation is reported but no suggestion is emitted.

  4. Test harness — replaced @typescript-eslint/rule-tester with RuleTester from eslint + Vitest describe/it, matching all sibling rules. Test cases updated to reflect the message-equivalence requirement (the different-message catch-block case is now in valid).


  • ✅ Reviews — all three in-scope reviewer threads addressed: message equivalence, annotation property exclusion, suggestion safety, and test harness corrected
  • ✅ Checks (local) — make fmt, TypeScript build, and npm test (our rule: 1/1 passing; pre-existing require-fs-io-try-catch failures are unrelated and unchanged)
  • ❓ Checks (CI) — stale after agent push; needs human re-trigger
  • ✅ Mergeable — branch is open and non-draft

Hand-off: CI must be re-triggered by a maintainer (close/reopen PR or push) before merge.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

…-setfailed

- Add same-object identifier check: c1.error("x"); c2.setFailed("x") with
  different aliases no longer triggers a false positive
- Fix Program body filter to exclude all module declarations: adds
  ExportNamedDeclaration and ExportDefaultDeclaration to the exclusion list
- Unify isCoreErrorStatement/isCoreSetFailedStatement into a single
  isCoreMethodCallStatement helper, eliminating the near-identical duplication
- Add getCoreObjectName helper to extract receiver name from a matched call
- Add test cases: mixed-receiver alias pair (valid) and non-core alias (valid)

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@copilot run pr-finisher skill

Done. Addressed all remaining github-actions[bot] review feedback in commit fix: address github-actions bot review feedback on no-core-error-then-setfailed:

  • Same-object checkc1.error("x"); c2.setFailed("x") where c1 and c2 are different CORE_ALIASES objects no longer triggers a false positive; added getCoreObjectName helper and a guard in checkStatements.
  • Program filter — now excludes all four module declaration types: ImportDeclaration, ExportAllDeclaration, ExportNamedDeclaration, and ExportDefaultDeclaration.
  • DRYisCoreErrorStatement and isCoreSetFailedStatement both delegate to a single isCoreMethodCallStatement(node, sourceCode, methodName) helper.
  • Tests — added valid cases for mixed-receiver aliases and non-core identifiers.

  • ✅ Reviews — all copilot-pull-request-reviewer and github-actions[bot] threads addressed
  • ✅ Checks (local) — make fmt, TypeScript build, npm test clean (pre-existing require-fs-io-try-catch failures unchanged); CodeQL 0 alerts
  • ❓ Checks (CI) — stale after agent push; needs human re-trigger
  • ✅ Mergeable — PR is open, non-draft, mergeable: clean

Hand-off: CI must be re-triggered by a maintainer (close/reopen PR or push) before merge.

Copilot AI requested a review from pelikhan July 27, 2026 13:07
@pelikhan
pelikhan merged commit 55b5181 into main Jul 27, 2026
@pelikhan
pelikhan deleted the eslint-miner/no-core-error-then-setfailed-f3ca87d2a9ef0f8d branch July 27, 2026 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants