Skip to content

fix(eslint-factory): suppress unsafe autofix for no-core-error-then-process-exit inside helper functions#46553

Merged
pelikhan merged 6 commits into
mainfrom
copilot/fix-eslint-autofix-issue
Jul 19, 2026
Merged

fix(eslint-factory): suppress unsafe autofix for no-core-error-then-process-exit inside helper functions#46553
pelikhan merged 6 commits into
mainfrom
copilot/fix-eslint-autofix-issue

Conversation

Copilot AI commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

The no-core-error-then-process-exit suggestion fix unconditionally replaced core.error(msg); process.exit(nonzero) with core.setFailed(msg); return; — even inside helper functions where return only exits the helper, letting callers continue with undefined instead of aborting the process.

Root cause

function requireEnvVar(name) {
  if (!value) {
    core.error(`ERROR: ${name} environment variable is required`);
    process.exit(1);  // ← autofix replaced this with `return;`
  }
  return value;       // caller now receives undefined instead of process dying
}

Changes

  • getImmediateEnclosingFunction — new helper to find the innermost enclosing function node via ancestor walk
  • isFunctionNamedMain — identifies main() entrypoints (function main, async function main, const main = () => {})
  • Conditional suggestionsuggest: [] (report-only) when the flagged pair is inside any function that is not main; the fix is only offered at module top-level or directly inside main() where return correctly terminates the entry path
  • Softened diagnostic message — removes the false claim that process.exit always "bypasses the lifecycle"; acknowledges that in standalone node scripts process.exit(nonzero) does fail the step

Safe vs. unsafe autofix contexts

Context Autofix offered?
Module top-level
async function main() / const main = async () => {}
Any other function (run, requireEnvVar, etc.) ❌ report-only

Warning

Firewall blocked 1 domain

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

  • pi.dev

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

network:
  allowed:
    - defaults
    - "pi.dev"

See Network Configuration for more information.

Generated by 👨‍🍳 PR Sous Chef · 8.59 AIC · ⌖ 8.26 AIC · ⊞ 7.5K ·
Comment /souschef to run again

… for non-main helper functions

The suggestion fix replacing `core.error(msg); process.exit(nonzero)` with
`core.setFailed(msg); return;` was unsafe inside helper functions: `return`
only exits the helper, not the process, silently converting a hard abort into
a fall-through that returns `undefined` to the caller.

Changes:
- Add `getImmediateEnclosingFunction` to find the innermost enclosing function
- Add `isFunctionNamedMain` to identify `main()` entrypoints
- Suppress the autofix suggestion when the pair is inside any function that is
  NOT named `main` (i.e., report-only with `suggest: []` for helpers)
- Soften the diagnostic message to acknowledge standalone-node contexts
- Update test: `function run()` now expects no suggestions
- Add tests: value-returning helper → no autofix; `async function main()` and
  `const main = async () => {}` entrypoints → autofix retained

Closes #46540

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix eslint-factory: no-core-error-then-process-exit autofix issue fix(eslint-factory): suppress unsafe autofix for no-core-error-then-process-exit inside helper functions Jul 19, 2026
Copilot AI requested a review from pelikhan July 19, 2026 07:21
@pelikhan
pelikhan marked this pull request as ready for review July 19, 2026 07:22
Copilot AI review requested due to automatic review settings July 19, 2026 07:22
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 19, 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 detected in src/, lib/, pkg/, internal/, app/, core/, domain/, services/, api/).

@github-actions

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

Prevents unsafe ESLint autofix suggestions inside helper functions while retaining fixes for top-level and main contexts.

Changes:

  • Detects the nearest enclosing function and named main functions.
  • Suppresses unsafe suggestions in non-entrypoint helpers.
  • Adds regression tests and clarifies diagnostics.
Show a summary per file
File Description
eslint-factory/src/rules/no-core-error-then-process-exit.ts Adds context-aware suggestion filtering.
eslint-factory/src/rules/no-core-error-then-process-exit.test.ts Tests safe and unsafe contexts.

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

Comment on lines +39 to +51
function isFunctionNamedMain(fn: FunctionNode): boolean {
if (fn.type === AST_NODE_TYPES.FunctionDeclaration) {
return fn.id?.name === "main";
}
// FunctionExpression or ArrowFunctionExpression assigned to a variable named `main`
const parent = fn.parent;
return (
parent != null &&
parent.type === AST_NODE_TYPES.VariableDeclarator &&
parent.id.type === AST_NODE_TYPES.Identifier &&
parent.id.name === "main"
);
}

@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 fix is correct and well-structured. The ancestor walk to find the innermost enclosing function is efficient, isFunctionNamedMain correctly covers all three main declaration styles (including export const main = ... since the ArrowFunctionExpression's parent is still a VariableDeclarator), and the safeToFix flag is cleanly reused inside the fixer instead of duplicating the walk. Test coverage addresses the core acceptance criteria. No blocking issues.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 10.8 AIC · ⌖ 4.33 AIC · ⊞ 5K

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

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

📊 Metrics (4 test cases)
Metric Value
Analyzed 4 (TypeScript vitest)
✅ Design tests 4 (100%)
⚠️ Implementation tests 0 (0%)
Edge/error coverage 4/4 (100%)
Duplicate clusters 0
Test inflation No (19 test lines : 73 source lines = 0.26:1)
🚨 Violations 0

Test Classification:

Test Case File Classification Coverage
Non-main helper function test.ts Design Edge case: validates that autofix is disabled for non-main functions
Value-returning helper test.ts Design Edge case: validates that autofix is disabled when helper returns a value
Async main() entrypoint test.ts Design Design: validates autofix is enabled for async main() functions
Arrow main entrypoint test.ts Design Design: validates autofix is enabled for arrow function main entries

Summary

Excellent test quality. All 4 new test cases are design tests that verify the rule's core behavioral contract: distinguishing safe autofix contexts (main entrypoint) from unsafe contexts (helper functions). The tests cover critical edge cases:

  • Helper functions where return does not abort the process
  • Return-value-bearing helpers where substituting return breaks the contract
  • Async function variants (function declaration and arrow)
  • Proper autofix suggestion injection for valid contexts

No test inflation, no duplicates, no forbidden patterns. The rule implementation changes (73 lines) are well-exercised by the test additions (19 lines).

Verdict

APPROVE. Test Quality Score: 100/100. Implementation tests: 0% (threshold: 30%). All new tests are high-confidence design tests.

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 · 17.3 AIC · ⌖ 8.8 AIC · ⊞ 7K ·
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: 100/100. 0% implementation tests (threshold: 30%). All new tests are high-confidence design tests with excellent edge-case coverage.

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

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

* Returns true when `fn` is a conventional module entrypoint named `main`:
* - `function main() {}` / `async function main() {}`
* - `const main = function() {}` / `const main = async () => {}`
*/

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.

[/diagnosing-bugs] isFunctionNamedMain does not verify that main is at module top-level — a nested function named main inside another function will incorrectly receive the autofix, where return only exits the inner main, not the outer function.

💡 Reproducer & suggested fix

Reproducer (currently gets unsafe autofix):

function outer() {
  async function main() {
    core.error("x");
    process.exit(1);  // ← autofix replaces with `return;`, but outer() continues
  }
  main();
}

Fix: after confirming the function is named main, also check that its own parent is a Program node (or a VariableDeclaration at Program level):

function isFunctionNamedMain(fn: FunctionNode): boolean {
  if (fn.type === AST_NODE_TYPES.FunctionDeclaration) {
    if (fn.id?.name !== "main") return false;
    return fn.parent?.type === AST_NODE_TYPES.Program;
  }
  const parent = fn.parent;
  if (
    parent?.type === AST_NODE_TYPES.VariableDeclarator &&
    parent.id.type === AST_NODE_TYPES.Identifier &&
    parent.id.name === "main"
  ) {
    // Walk up: VariableDeclarator → VariableDeclaration → must be Program
    return parent.parent?.parent?.type === AST_NODE_TYPES.Program;
  }
  return false;
}

Add a corresponding test case:

{
  // nested main() is NOT a safe entrypoint — no autofix
  code: `function outer() { async function main() { core.error("x"); process.exit(1); } }`,
  errors: [{ messageId: "noCoreErrorThenProcessExit", suggestions: [] }],
},

@copilot please address this.

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.

Fixed in dbd04a0. isFunctionNamedMain now checks fn.parent?.type === AST_NODE_TYPES.Program for FunctionDeclaration, and for variable-assigned forms walks VariableDeclarator → VariableDeclaration and confirms varDecl.parent?.type === AST_NODE_TYPES.Program. Both nested forms now produce suggestions: [].

errors: [{ messageId: "noCoreErrorThenProcessExit", suggestions: [{ messageId: "replaceWithSetFailed", output: "core.setFailed(`ERROR: ${message}`);\n " }] }],
},
{
// pair inside a non-main function: no autofix because `return` only exits the helper,

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] No test covers the module top-level case (pair outside any function, enclosingFn === null), which is the other path that enables the autofix.

💡 Suggested test
{
  // module top-level: no enclosing function, autofix is safe
  code: `core.error("fatal"); process.exit(1);`,
  errors: [{ messageId: "noCoreErrorThenProcessExit", suggestions: [{ messageId: "replaceWithSetFailed", output: `core.setFailed("fatal");\n` }] }],
},

Without this, a future regression that accidentally suppresses top-level autofixes would go undetected.

@copilot please address this.

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.

Added in 7149980: an explicit test case labeled // module top-level: no enclosing function (enclosingFn === null), autofix is safe is now the first invalid case in the suite, directly documenting the enclosingFn === null path. The isInsideFunction variable was also inlined with a clarifying comment.

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

REQUEST_CHANGES — one critical scope-check bug blocks merge

The fix correctly identifies the core problem (unconditional autofix inside helper functions) but introduces an incomplete guard: isFunctionNamedMain checks the variable name but not the variable scope, so a locally-nested const main = async () => {} inside another function still receives an unsafe autofix suggestion.

Findings detail

CriticalisFunctionNamedMain is scope-unaware (inline comment on line 77 of the rule): any VariableDeclarator whose id is "main" anywhere in the AST passes the check — including one nested 3 levels deep inside another function. The same gap exists for function main() declarations, which have no Program-parent guard either. Fix: verify declaration.parent.type === AST_NODE_TYPES.Program before returning true.

Medium — No test for the nested-main false-positive. The bug is invisible to CI. Suggested test case added in the inline comment on line 76 of the test file.

LowisInsideFunction inside the fix closure is now always enclosingFn !== null, which is always true when safeToFix is true and we are in a main function. The variable name no longer matches its meaning and will mislead future readers.

🔎 Code quality review by PR Code Quality Reviewer · 38.5 AIC · ⌖ 4.71 AIC · ⊞ 5.6K
Comment /review to run again

Comments that could not be inline-anchored

eslint-factory/src/rules/no-core-error-then-process-exit.ts:77

Nested const main passes the scope check regardless of nesting depth, producing an unsafe autofix: isFunctionNamedMain only verifies the variable is named main but never checks the VariableDeclarator is at module top-level. A locally-scoped const main = async () =&gt; { core.error(&quot;x&quot;); process.exit(1); } inside another function will receive the autofix suggestion, yet return from that inner arrow does NOT exit the process.

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

After find…

eslint-factory/src/rules/no-core-error-then-process-exit.test.ts:76

No test for the nested-main false-positive: the test suite doesn't cover const main = async () =&gt; {} inside another function. Given the scope-check gap in isFunctionNamedMain, this means the regression where a nested main receives an unsafe autofix is completely undetected.

<details>
<summary>💡 Suggested test case to add</summary>

{
  // nested const main inside a factory function — NOT an entrypoint; no autofix
  code: `function createRunner() { const main = async () =&gt; { 

</details>

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Copilot AI and others added 2 commits July 19, 2026 09:06
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address the remaining requested-review feedback, and rerun checks. The key blocker is the scope check around main: ensure only top-level main entrypoints get the autofix, add coverage for nested main false positives, and clear any lingering review threads before merge.

Warning

Firewall blocked 1 domain

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

  • pi.dev

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

network:
  allowed:
    - defaults
    - "pi.dev"

See Network Configuration for more information.

Generated by 👨‍🍳 PR Sous Chef · 8.59 AIC · ⌖ 8.26 AIC · ⊞ 7.5K ·
Comment /souschef to run again

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

Copilot AI commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Addressed in dbd04a0: isFunctionNamedMain now checks that the function is a direct child of Program before treating it as an entrypoint. For FunctionDeclaration this means fn.parent?.type === AST_NODE_TYPES.Program, and for the variable-assigned form it walks up through VariableDeclarator → VariableDeclaration and checks that the VariableDeclaration.parent is Program. Two regression tests cover both nested-function main() and nested-const main = async () => {} cases — both now correctly produce suggestions: [].

…ixer logic comment

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

Copilot AI commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address the remaining requested-review feedback, and rerun checks. The key blocker is the scope check around main: ensure only top-level main entrypoin...

All remaining feedback addressed (7149980):

  • isFunctionNamedMain now enforces module-scope for both function main() and const main = () => {} forms
  • Two regression tests added for nested main (both declaration and arrow forms return suggestions: [])
  • Explicit module top-level test added documenting the enclosingFn === null path
  • isInsideFunction variable inlined with a clarifying comment

Local validation: make fmt + make lint + 296 vitest tests all pass. Parallel code review and CodeQL scan both clean. Ready for merge after CI re-trigger.

Copilot AI requested a review from gh-aw-bot July 19, 2026 09:20
@pelikhan
pelikhan merged commit 93b176c into main Jul 19, 2026
@pelikhan
pelikhan deleted the copilot/fix-eslint-autofix-issue branch July 19, 2026 09:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants