Summary
The two sibling rules that flag core.error(...) followed by a failure signal diverge in detection strength, producing a false negative in the process.exitCode variant:
no-core-error-then-process-exit scans forward from core.error(...) (checkStatements, inner for (j = i+1; ...)) for a later process.exit(nonzero), stopping only at a core.setFailed(...) or a control-transfer statement. It correctly catches core.error(); <intervening stmt>; process.exit(1).
no-core-error-then-process-exitcode checks only the immediately adjacent statement: isCoreErrorStatement(current) && isProcessExitCodeNonZero(stmts[i+1]). Any intervening statement defeats detection.
This asymmetry is especially wrong for the exitCode variant: unlike process.exit(1), process.exitCode = 1 does not halt execution, so it is common to have statements around it. A benign line between the error and the assignment silently disables the rule:
core.error("deploy failed");
core.info("cleaning up"); // any intervening statement
process.exitCode = 1; // NOT flagged today — false negative
The sibling no-core-error-then-process-exit would flag the analogous process.exit(1) case. (The existing valid test core.error("msg"); process.exit(1); at line 33 and the exports-break-adjacency test at line 31 confirm adjacency-only is the current, intentional-but-narrow behavior; there is no test for a benign intervening statement.)
Secondary gap: missing top-level autofix
no-core-error-then-process-exit offers its replaceWithSetFailed autofix at module top level (enclosingFn === null → core.setFailed(args); with no return;). no-core-error-then-process-exitcode gates safeToFix on isFunctionNamedMain(enclosingFn), so at module top level it emits no suggestion — even though process.exitCode = 1 at top level is safely replaceable by core.setFailed(msg); (which also sets exitCode = 1). This is an avoidable parity gap.
Grounding
Soundness/consistency fix; no live core.error(); ...; process.exitCode = nonzero occurrence found in the current actions/setup/js/** corpus (the flagged pairs today are process.exit, e.g. convert_gateway_config_shared.cjs:48-49 and :70-71, correctly caught by the sibling). Filing to guard against regressions and to bring the two sibling rules to behavioral parity — cheap, self-contained.
Acceptance criteria
no-core-error-then-process-exitcode scans forward from core.error(...) for a subsequent process.exitCode = <nonzero literal>, stopping the scan at core.setFailed(...) or a control-transfer statement (mirroring the exit sibling's stop conditions).
- New invalid test:
core.error("x"); core.info("y"); process.exitCode = 1; reports once.
- New valid tests preserved:
core.error("x"); core.setFailed("y"); process.exitCode = 1; and core.error("x"); return; process.exitCode = 1; do NOT report (setFailed / control-transfer breaks the scan). Existing valid cases (exitCode = 0, variable RHS, +=, non-core object, exports-break-adjacency at module scope) stay green.
- Non-adjacent reports omit the autofix (same convention the exit sibling uses for non-adjacent pairs) to avoid a fixer spanning intervening statements.
- Add the module-top-level autofix (
enclosingFn === null → replace with core.setFailed(args);, no return;) for the adjacent case, matching the exit sibling.
- Update the rule docs to describe the forward-scan semantics.
Filed by the ESLint Refiner daily review.
Generated by 🤖 ESLint Refiner · age00 312 AIC · ⌖ 12.9 AIC · ⊞ 4.6K · ◷
Summary
The two sibling rules that flag
core.error(...)followed by a failure signal diverge in detection strength, producing a false negative in theprocess.exitCodevariant:no-core-error-then-process-exitscans forward fromcore.error(...)(checkStatements, innerfor (j = i+1; ...)) for a laterprocess.exit(nonzero), stopping only at acore.setFailed(...)or a control-transfer statement. It correctly catchescore.error(); <intervening stmt>; process.exit(1).no-core-error-then-process-exitcodechecks only the immediately adjacent statement:isCoreErrorStatement(current) && isProcessExitCodeNonZero(stmts[i+1]). Any intervening statement defeats detection.This asymmetry is especially wrong for the
exitCodevariant: unlikeprocess.exit(1),process.exitCode = 1does not halt execution, so it is common to have statements around it. A benign line between the error and the assignment silently disables the rule:The sibling
no-core-error-then-process-exitwould flag the analogousprocess.exit(1)case. (The existing valid testcore.error("msg"); process.exit(1);at line 33 and the exports-break-adjacency test at line 31 confirm adjacency-only is the current, intentional-but-narrow behavior; there is no test for a benign intervening statement.)Secondary gap: missing top-level autofix
no-core-error-then-process-exitoffers itsreplaceWithSetFailedautofix at module top level (enclosingFn === null→core.setFailed(args);with noreturn;).no-core-error-then-process-exitcodegatessafeToFixonisFunctionNamedMain(enclosingFn), so at module top level it emits no suggestion — even thoughprocess.exitCode = 1at top level is safely replaceable bycore.setFailed(msg);(which also sets exitCode = 1). This is an avoidable parity gap.Grounding
Soundness/consistency fix; no live
core.error(); ...; process.exitCode = nonzerooccurrence found in the currentactions/setup/js/**corpus (the flagged pairs today areprocess.exit, e.g.convert_gateway_config_shared.cjs:48-49and:70-71, correctly caught by the sibling). Filing to guard against regressions and to bring the two sibling rules to behavioral parity — cheap, self-contained.Acceptance criteria
no-core-error-then-process-exitcodescans forward fromcore.error(...)for a subsequentprocess.exitCode = <nonzero literal>, stopping the scan atcore.setFailed(...)or a control-transfer statement (mirroring the exit sibling's stop conditions).core.error("x"); core.info("y"); process.exitCode = 1;reports once.core.error("x"); core.setFailed("y"); process.exitCode = 1;andcore.error("x"); return; process.exitCode = 1;do NOT report (setFailed / control-transfer breaks the scan). Existing valid cases (exitCode = 0, variable RHS,+=, non-core object, exports-break-adjacency at module scope) stay green.enclosingFn === null→ replace withcore.setFailed(args);, noreturn;) for the adjacent case, matching the exit sibling.Filed by the ESLint Refiner daily review.