Skip to content

eslint-factory: detect aliased/destructured/computed core.setFailed in require-return-after-core-setfailed#45334

Merged
pelikhan merged 2 commits into
mainfrom
copilot/eslint-factory-require-return-after-core-setfailed
Jul 14, 2026
Merged

eslint-factory: detect aliased/destructured/computed core.setFailed in require-return-after-core-setfailed#45334
pelikhan merged 2 commits into
mainfrom
copilot/eslint-factory-require-return-after-core-setfailed

Conversation

Copilot AI commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

require-return-after-core-setfailed only matched the literal core.setFailed(...) form, silently missing three escape patterns where execution continues after failure is declared.

New detection forms

  • Computed member: core["setFailed"](msg) — string-literal property check
  • Aliased object: const c = core; c.setFailed(msg) — scope-walks to find single-assignment const/let aliases of core-like identifiers; rejects re-assigned bindings
  • Destructured binding: const { setFailed } = core; setFailed(msg) — ObjectPattern analysis; also handles renamed destructure (const { setFailed: sf } = core; sf(msg))

All three forms participate in the same cross-block continuation and suggestion (addReturn) logic as the direct form.

Locally-shadowed setFailed bindings (function setFailed() {}, non-core const setFailed = ...) are excluded via def.type !== "Variable" and origin-identity guards.

Implementation notes

isCoreSetFailedStatement is moved into create(context) as a scope-aware closure (same pattern as require-await-core-summary-write). CORE_ALIASES / isCoreLikeIdentifier are shared at the top level for consistency. checkStatementList now accepts the check function as a parameter.

const c = core;
const { setFailed } = core;

function run() {
  c.setFailed("bad");         // now flagged — aliased object
  doMore();

  setFailed("oops");          // now flagged — destructured binding
  keepGoing();

  core["setFailed"]("err");   // now flagged — computed member
  andMore();
}

…iled in require-return-after-core-setfailed

- Detect core["setFailed"](...) (computed member with string literal)
- Detect aliased object: const c = core; c.setFailed(...)
- Detect destructured binding: const { setFailed } = core; setFailed(...)
- Detect renamed destructure: const { setFailed: sf } = core; sf(...)
- Exclude locally-shadowed setFailed bindings (function/parameter/non-core const)
- Add 7 new test cases covering all new forms and shadow exclusion

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix false negatives in require-return-after-core-setfailed rule eslint-factory: detect aliased/destructured/computed core.setFailed in require-return-after-core-setfailed Jul 13, 2026
Copilot AI requested a review from pelikhan July 13, 2026 21:55
@pelikhan pelikhan marked this pull request as ready for review July 14, 2026 02:39
Copilot AI review requested due to automatic review settings July 14, 2026 02:39
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

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

No ADR enforcement needed: PR #45334 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 14, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🧠 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

Extends the ESLint rule to detect computed, aliased, and destructured core.setFailed calls.

Changes:

  • Adds scope-aware alias and destructuring detection.
  • Adds tests for primary detection forms and suggestions.
  • Preserves existing cross-block continuation checks.
Show a summary per file
File Description
require-return-after-core-setfailed.ts Implements expanded detection.
require-return-after-core-setfailed.test.ts Tests new call forms.

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

});
});

it("valid: locally-shadowed setFailed bindings are not flagged", () => {
@github-actions github-actions Bot mentioned this pull request Jul 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 87/100 — Excellent

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

📊 Metrics (7 tests)
Metric Value
Analyzed 7 (Go: 0, JS: 7)
✅ Design 7 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 4 (57%)
Duplicate clusters 0
Inflation No (0.87:1)
🚨 Violations 0
Test File Classification Issues
valid: computed core["setFailed"] followed by return require-return-after-core-setfailed.test.ts design_test / high_value None
invalid: computed core["setFailed"] without control transfer require-return-after-core-setfailed.test.ts design_test / high_value None
valid: aliased core object with return require-return-after-core-setfailed.test.ts design_test / high_value None
invalid: aliased core object without control transfer require-return-after-core-setfailed.test.ts design_test / high_value None
valid: destructured setFailed from core with return require-return-after-core-setfailed.test.ts design_test / high_value None
invalid: destructured setFailed from core without control transfer require-return-after-core-setfailed.test.ts design_test / high_value None
valid: locally-shadowed setFailed bindings are not flagged require-return-after-core-setfailed.test.ts design_test / high_value None

Verdict

Passed. 0% implementation tests (threshold: 30%). All 7 new tests are behavioral design tests covering computed member access, aliased objects, destructured bindings, renamed destructures, and shadow exclusion — each paired with valid/invalid cases that verify both the lint rule trigger and the suggested fix output.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 27.8 AIC · ⌖ 7.86 AIC · ⊞ 6.8K ·
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: 87/100. 0% implementation tests (threshold: 30%).

@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 implementation is correct and well-tested. The scope-chain walk pattern for alias/destructure detection is sound, the re-assignment guard properly rejects mutable bindings, and shadow exclusion via def.type !== "Variable" correctly handles local function and parameter shadows. Test coverage spans all three new detection forms plus negative/shadow cases. No blocking issues found.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 13 AIC · ⌖ 5.13 AIC · ⊞ 4.8K

@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 — leaving as COMMENT with three actionable suggestions.

Key Themes

  • Code duplication: isCoreAliasIdentifier and isDestructuredSetFailedIdentifier repeat identical scope-chain traversal boilerplate. A shared resolveVariableDef helper would remove ~20 lines of duplication (see inline comment).
  • Missing regression tests: the re-assignment guard (isWrite() && !ref.init) and the coreObj alias in CORE_ALIASES are untested (see inline comments).

Positive Highlights

  • Scope-aware detection correctly mirrors the require-await-core-summary-write pattern.
  • Conservative false-positive guards (def-type check, re-assignment rejection, origin-identity guard).
  • Comprehensive happy-path and shadowing tests covering all three new forms.
  • checkStatementList parametrization is a clean seam improvement.

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 30.8 AIC · ⌖ 4.67 AIC · ⊞ 6.6K
Comment /matt to run again

/**
* Checks whether an Identifier is the destructured `setFailed` binding from
* a core-like object (e.g., `const { setFailed } = core` or
* `const { setFailed: sf } = core` where `sf` is the identifier).

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] isCoreAliasIdentifier and isDestructuredSetFailedIdentifier repeat the same scope-chain walk (lines ~179–193 vs ~210–224). Extracting a shared helper would make each function shallower and easier to test in isolation.

💡 Suggested refactor
function resolveVariableDef(identifier: TSESTree.Identifier): TSESTree.VariableDeclarator | null {
  let scope: TSESLint.Scope.Scope | null = sourceCode.getScope(identifier);
  while (scope !== null) {
    const variable = scope.set.get(identifier.name);
    if (variable !== undefined) {
      if (variable.defs.length !== 1) return null;
      const def = variable.defs[0];
      if (def.type !== "Variable") return null;
      if (variable.references.some(ref => ref.isWrite() && !ref.init)) return null;
      const declarator = def.node as TSESTree.VariableDeclarator;
      return declarator.init ? declarator : null;
    }
    scope = scope.upper;
  }
  return null;
}

Both helpers then call resolveVariableDef and only differ in what they assert about the resulting declarator.

@copilot please address this.

code: `const c = core; function f() { c["setFailed"]("bad"); doMore(); keepGoing(); }`,
errors: [{ messageId: "missingReturnAfterSetFailed", suggestions: [{ messageId: "addReturn", output: `const c = core; function f() { c["setFailed"]("bad"); return; doMore(); keepGoing(); }` }] }],
},
],

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 re-assigned binding guard (variable.references.some(ref => ref.isWrite() && !ref.init)) is untested. A test with let c = core; c = other; would confirm the rule does NOT flag it.

💡 Suggested test
it("valid: re-assigned alias is not flagged", () => {
  ruleTester.run("require-return-after-core-setfailed", requireReturnAfterCoreSetFailedRule, {
    valid: [
      // c is re-assigned after initialisation — not a reliable core alias
      `let c = core; c = other; function f() { c.setFailed("bad"); doMore(); }`,
    ],
    invalid: [],
  });
});

Without this test, a future refactor that accidentally drops the re-assignment guard would pass all existing tests.

@copilot please address this.

return obj.type === AST_NODE_TYPES.Identifier && obj.name === "core" && prop.type === AST_NODE_TYPES.Identifier && prop.name === "setFailed";
// Known @actions/core binding names (same allow-list as require-await-core-summary-write).
// Only exact known aliases are matched — broad prefix matching (e.g. `/^core/i`) would
// silently flag unrelated objects that happen to start with "core".

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] coreObj is in CORE_ALIASES but has no test exercising it. If it is a live alias used in the codebase, add a test; if it is aspirational, remove it to avoid false coverage confidence.

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

Two correctness gaps and two maintainability issues to fix before merge

Blocking:

  1. isDestructuredSetFailedIdentifier never calls isCoreAliasIdentifier on its init — const c = core; const { setFailed } = c; setFailed("bad"); doMore(); is a silent false negative that contradicts the stated goal of this PR.
  2. isCoreAliasIdentifier is single-depth only — const d = c; d.setFailed(...) is also silently missed.

Non-blocking but should be addressed:
3. CORE_ALIASES is a mutable copy-pasted Set — should be a shared frozen constant with require-await-core-summary-write to prevent silent divergence.
4. coreObj patterns are completely untested — if the entry were removed from CORE_ALIASES, no test would fail.

Passing findings

The core pattern (computed core["setFailed"], scope-walk for aliases, ObjectPattern destructuring, shadow exclusion) is sound. The scope-chain iteration with re-assignment rejection is correct. The checkStatementList predicate injection is a clean refactor.

🔎 Code quality review by PR Code Quality Reviewer · 52.8 AIC · ⌖ 4.9 AIC · ⊞ 5.4K
Comment /review to run again

Comments that could not be inline-anchored

eslint-factory/src/rules/require-return-after-core-setfailed.ts:190

False negative: isDestructuredSetFailedIdentifier never resolves aliased inits. const c = core; const { setFailed } = c; setFailed(&#39;bad&#39;); doMore(); will not be flagged — the init is c, which passes isCoreLikeIdentifier only for literal &quot;core&quot;/&quot;coreObj&quot;, not for aliases.

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

// In isDestructuredSetFailedIdentifier, replace:
if (declarator.id.type === AST_NODE_TYPES.ObjectPattern &amp;&amp;
    declarator.init.type === AST_NODE_TY…

</details>

<details><summary>eslint-factory/src/rules/require-return-after-core-setfailed.ts:165</summary>

**Single-depth alias only: chained aliases are silently missed.** `const c = core; const d = c; d.setFailed(&#39;bad&#39;); doMore();` will not be flagged because `isCoreAliasIdentifier` checks `isCoreLikeIdentifier(declarator.init.name)` but `c` is not in `CORE_ALIASES`.

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

```typescript
function isCoreAliasIdentifier(identifier: TSESTree.Identifier): boolean {
  // ... (existing scope walk) ...
  const initName = declarator.init.name;
  // Recurse: allow c…

</details>

<details><summary>eslint-factory/src/rules/require-return-after-core-setfailed.ts:124</summary>

**`CORE_ALIASES` is a mutable module-level `Set` — and is copy-pasted rather than shared.** `const` only prevents reassignment; `.add()`/`.delete()` still work. Any test or external code that mutates it affects all subsequent evaluations in the same process. Separately, the comment says &#39;same allow-list as `require-await-core-summary-write`&#39; but the `Set` is copied, not imported — the two rules will silently diverge as soon as one is updated.

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

Extr…

</details>

<details><summary>eslint-factory/src/rules/require-return-after-core-setfailed.test.ts:29</summary>

**Zero test coverage for `coreObj` — the second alias in `CORE_ALIASES` is dead from a test perspective.** Every added test uses `core`; none exercises `coreObj.setFailed(...)`, `coreObj[&#39;setFailed&#39;](...)`, aliased `coreObj`, or destructured `coreObj`. If the `coreObj` entry were silently removed from `CORE_ALIASES`, no test would fail.

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

Add at minimum one valid and one invalid test case using `coreObj`:

```typescript
it(&#39;invalid: coreObj.setFaile…

</details>

@pelikhan pelikhan merged commit 1c6fcaf into main Jul 14, 2026
59 checks passed
@pelikhan pelikhan deleted the copilot/eslint-factory-require-return-after-core-setfailed branch July 14, 2026 03:03
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: require-return-after-core-setfailed misses aliased/destructured core.setFailed

3 participants