From ae379e7e128563252644cf3d266b60faafbac0c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 15 Aug 2026 22:15:40 +0000 Subject: [PATCH 1/2] Initial plan From 1b0cf083cdae761c2c1f21813356b3dc48fdc67e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 15 Aug 2026 22:22:48 +0000 Subject: [PATCH 2/2] Require positive sync exec timeouts Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../rules/require-sync-exec-timeout.test.ts | 20 +++++++++++++++++-- .../src/rules/require-sync-exec-timeout.ts | 12 +++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/eslint-factory/src/rules/require-sync-exec-timeout.test.ts b/eslint-factory/src/rules/require-sync-exec-timeout.test.ts index 903929a443c..2b885bf2aa4 100644 --- a/eslint-factory/src/rules/require-sync-exec-timeout.test.ts +++ b/eslint-factory/src/rules/require-sync-exec-timeout.test.ts @@ -38,9 +38,13 @@ describe("require-sync-exec-timeout", () => { }); }); - it("valid: options passed via identifier or spread are not statically inspectable", () => { + it("valid: non-literal timeout values, options identifiers, and spreads are not statically inspectable", () => { cjsRuleTester.run("require-sync-exec-timeout", requireSyncExecTimeoutRule, { - valid: [`const { execSync } = require("child_process"); const opts = { timeout: 5000 }; execSync("git status", opts);`, `const { execSync } = require("child_process"); const base = {}; execSync("git status", { ...base });`], + valid: [ + `const { execSync } = require("child_process"); execSync("git status", { timeout: userConfig.timeout });`, + `const { execSync } = require("child_process"); const opts = { timeout: 5000 }; execSync("git status", opts);`, + `const { execSync } = require("child_process"); const base = {}; execSync("git status", { ...base });`, + ], invalid: [], }); }); @@ -68,6 +72,18 @@ describe("require-sync-exec-timeout", () => { code: `const { execSync } = require("child_process"); execSync("git status", { timeout: undefined });`, errors: [{ messageId: "requireTimeout" }], }, + { + code: `const { execSync } = require("child_process"); execSync("git status", { timeout: 0 });`, + errors: [{ messageId: "requireTimeout" }], + }, + { + code: `const { execSync } = require("child_process"); execSync("git status", { timeout: -1 });`, + errors: [{ messageId: "requireTimeout" }], + }, + { + code: `const { execSync } = require("child_process"); execSync("git status", { timeout: -0 });`, + errors: [{ messageId: "requireTimeout" }], + }, ], }); }); diff --git a/eslint-factory/src/rules/require-sync-exec-timeout.ts b/eslint-factory/src/rules/require-sync-exec-timeout.ts index ea8afcb7d3b..1a7d202ae8e 100644 --- a/eslint-factory/src/rules/require-sync-exec-timeout.ts +++ b/eslint-factory/src/rules/require-sync-exec-timeout.ts @@ -109,7 +109,7 @@ function resolveSyncExecMethod(node: TSESTree.CallExpression, sourceCode: TSESLi return null; } -/** Returns true when the options-object argument for the call statically carries a non-nullish `timeout` property. */ +/** Returns true when the options-object argument for the call statically carries a positive `timeout` property. */ function hasTimeoutOption(node: TSESTree.CallExpression, method: SyncExecMethod): boolean { const optionsArg = getOptionsArgument(node, method); if (!optionsArg) return false; @@ -127,8 +127,11 @@ function hasTimeoutOption(node: TSESTree.CallExpression, method: SyncExecMethod) if (!isTimeoutProp) continue; const value = prop.value; - const isNullish = (value.type === AST_NODE_TYPES.Literal && value.value == null) || (value.type === AST_NODE_TYPES.Identifier && value.name === "undefined"); - if (!isNullish) return true; + const isMissingTimeout = + (value.type === AST_NODE_TYPES.Literal && (value.value == null || (typeof value.value === "number" && value.value <= 0))) || + (value.type === AST_NODE_TYPES.UnaryExpression && value.operator === "-" && value.argument.type === AST_NODE_TYPES.Literal && typeof value.argument.value === "number") || + (value.type === AST_NODE_TYPES.Identifier && value.name === "undefined"); + if (!isMissingTimeout) return true; } return false; @@ -147,7 +150,8 @@ export const requireSyncExecTimeoutRule = createRule({ }, schema: [], messages: { - requireTimeout: "{{method}}({{arg}}) has no `timeout` option. Pass `{ timeout: , ...otherOptions }` so a hung or runaway child process cannot block the job indefinitely.", + requireTimeout: + "{{method}}({{arg}}) has no positive `timeout` option. `timeout: 0` disables the timeout; pass `{ timeout: , ...otherOptions }` so a hung or runaway child process cannot block the job indefinitely.", }, }, defaultOptions: [],