[eslint-miner] eslint: add require-execsync-try-catch rule - #46789
Conversation
execSync from child_process throws a ChildProcessError when the child
process exits non-zero or is killed by a signal. An unhandled throw
crashes the action step with no useful diagnostic.
Evidence in actions/setup/js:
- start_mcp_gateway.cjs:824 — bare execSync(`node "${converterPath}"`)
without a surrounding try/catch (confirmed by lint:setup-js producing
one new warning for this exact line)
The rule:
- Detects execSync sourced from child_process / node:child_process via
destructured require, namespace require, or ESM import
- Skips calls already inside a try block
- Offers a hasSuggestions fixer to wrap the statement in try/catch with
{ cause: err } re-throw
- Zero false positives: execSync from unrelated modules and bare
(unresolved) identifiers are ignored
Tests: 8 new unit tests (valid + invalid cases); all 306 tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR #46789 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100). |
|
|
|
✅ Test Quality Sentinel completed test quality analysis. |
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
There was a problem hiding this comment.
Pull request overview
Adds an ESLint rule requiring child_process.execSync calls to be protected by try/catch.
Changes:
- Implements binding-aware detection and remediation suggestions.
- Adds RuleTester coverage.
- Registers and enables the rule.
Show a summary per file
| File | Description |
|---|---|
eslint-factory/src/rules/require-execsync-try-catch.ts |
Implements the rule. |
eslint-factory/src/rules/require-execsync-try-catch.test.ts |
Tests supported call patterns. |
eslint-factory/src/index.ts |
Exports the rule. |
eslint-factory/eslint.config.cjs |
Enables the rule as a warning. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (1)
eslint-factory/src/rules/require-execsync-try-catch.ts:120
- String-literal member access such as
cp["execSync"](...)is semantically the same namespace call, but!callee.computedmakes it invisible to this rule. The shared fs resolver already handles this shape intry-catch-rule-utils.ts:138-145; resolve both identifier and computed string-literal properties here and cover the case in tests.
callee.type === AST_NODE_TYPES.MemberExpression &&
!callee.computed &&
callee.object.type === AST_NODE_TYPES.Identifier &&
callee.property.type === AST_NODE_TYPES.Identifier &&
callee.property.name === "execSync"
- Files reviewed: 4/4 changed files
- Comments generated: 3
- Review effort level: Medium
| if (isChildProcessImportBinding(def) && def.node.type === AST_NODE_TYPES.ImportNamespaceSpecifier) { | ||
| return true; |
| // Still flag it even in deferred callbacks — execSync in async callbacks is still risky. | ||
| void withinDeferredBoundary; |
| docs: { | ||
| description: | ||
| "Require execSync calls in actions/setup/js scripts to be wrapped in try/catch. " + | ||
| "execSync throws a ChildProcessError when the child process exits with a non-zero status code or is killed by a signal; " + |
There was a problem hiding this comment.
Review: require-execsync-try-catch
The rule logic, scope analysis, and test coverage are solid. One blocking issue:
Dead code must be removed — the withinDeferredBoundary ancestor-walk in CallExpression is computed then immediately discarded (void withinDeferredBoundary). Per the inline comment this is intentional ("Still flag it even in deferred callbacks"), making the entire block a no-op that burns CPU on every flagged call site and leaves misleading code behind. Remove it before merging.
🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 22 AIC · ⌖ 4.46 AIC · ⊞ 5K
| const stmt = findEnclosingStatement(sourceCode, node); | ||
|
|
||
| context.report({ | ||
| node, |
There was a problem hiding this comment.
Dead code: withinDeferredBoundary is computed but never used.
The block below computes withinDeferredBoundary by walking all ancestors, then immediately discards the result via void withinDeferredBoundary. The accompanying comment says "Still flag it even in deferred callbacks", which means this whole block is a no-op — it adds CPU overhead on every matched call site without affecting behaviour.
Remove the unused variable and its ancestor-walk entirely:
// DELETE all of this:
const ancestors = sourceCode.getAncestors(node);
let withinDeferredBoundary = false;
for (let i = ancestors.length - 1; i >= 0; i--) {
if (isDeferredCallback(ancestors[i])) {
withinDeferredBoundary = true;
break;
}
}
void withinDeferredBoundary;If there is future intent to skip flagging in deferred callbacks, track that as a follow-up issue rather than leaving inert code here.
@copilot please address this.
There was a problem hiding this comment.
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 28.3 AIC · ⌖ 4.54 AIC · ⊞ 6.7K
Comment /matt to run again
| } | ||
| } | ||
| // Still flag it even in deferred callbacks — execSync in async callbacks is still risky. | ||
| void withinDeferredBoundary; |
There was a problem hiding this comment.
[/codebase-design] Dead code: the withinDeferredBoundary block (lines 159–168) is computed then immediately suppressed with void — it has no effect on rule behaviour.
💡 Suggestion
The inline comment says "Still flag it even in deferred callbacks", so the intent is clear, but the code is misleading noise. Either:
- Remove the block entirely (preferred — the behaviour is already correct without it):
// Remove lines 159–168; isInsideTryBlock already handles the try-protection check.- Or add a single explanatory comment where
isInsideTryBlockis called:
// execSync inside deferred callbacks is still flagged — the outer try does not protect it.
if (isInsideTryBlock(sourceCode, node)) return;Dead void-suppressed variables indicate unfinished refactoring and may confuse future maintainers.
@copilot please address this.
| ], | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
[/tdd] Missing test: ESM namespace import (import * as cp from "child_process") is not covered — the rule handles ImportNamespaceSpecifier in isChildProcessObjectBinding but there is no test confirming it works.
💡 Suggested test case
it("invalid: cp.execSync without try/catch (ESM namespace import)", () => {
esmRuleTester.run("require-execsync-try-catch", requireExecSyncTryCatchRule, {
valid: [
`import * as cp from "child_process"; try { cp.execSync("ls"); } catch (e) {}`,
],
invalid: [
{
code: `import * as cp from "child_process"; cp.execSync("ls");`,
errors: [{ messageId: "requireTryCatch" }],
},
],
});
});Without this test, a regression in the namespace import path would go undetected.
@copilot please address this.
🧪 Test Quality Sentinel Report✅ Test Quality Score: 89/100 — Excellent
📊 Metrics (8 tests)
Verdict
Strengths:
Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
There was a problem hiding this comment.
✅ Test Quality Sentinel: 89/100 Excellent. 0% implementation tests (threshold: 30%). All 8 tests verify design-level behavioral contracts with comprehensive edge case coverage for binding resolution and error detection across CommonJS and ES modules.
|
Commit pushed:
|
|
@copilot please run the Failed checks: Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "pi.dev"See Network Configuration for more information.
|
|
🎉 This pull request is included in a new release. Release: |
Summary
Adds a new ESLint rule
gh-aw-custom/require-execsync-try-catchto theeslint-factoryplugin. The rule flags any call toexecSyncfromchild_process(ornode:child_process) that is not wrapped in atry/catchblock. An unhandledexecSyncthrow crashes the action without surfacing a useful diagnostic.Changes
eslint-factory/src/rules/require-execsync-try-catch.tsexecSyncbindings across CJS destructured, CJS namespace, ESM named imports, and aliased formseslint-factory/src/rules/require-execsync-try-catch.test.tsRuleTestersuite covering valid and invalid patterns for all import styles, aliases, and non-child_processsourceseslint-factory/src/index.tseslint-factory/eslint.config.cjsgh-aw-custom/require-execsync-try-catchatwarnseverityRule behaviour
execSync(...)call fromchild_process/node:child_processnot enclosed in atryblock.Impact
warn; existing code is not blocked.node:protocol specifiers.