diff --git a/eslint-factory/README.md b/eslint-factory/README.md index c60405a0832..6ae7017e0d9 100644 --- a/eslint-factory/README.md +++ b/eslint-factory/README.md @@ -38,6 +38,8 @@ This project hosts custom ESLint linters for `/actions/setup/js`. | [`require-new-url-try-catch`](#require-new-url-try-catch) | Require try/catch around `new URL(variable)` calls | | [`require-parseInt-radix`](#require-parseInt-radix) | Require an explicit radix argument to `parseInt()` | | [`require-return-after-core-setfailed`](#require-return-after-core-setfailed) | Require a control-transfer statement after `core.setFailed()` | +| [`require-execsync-try-catch`](#require-execsync-try-catch) | Require try/catch around `execSync(...)` calls from `child_process` | +| [`require-execfilesync-try-catch`](#require-execfilesync-try-catch) | Require try/catch around `execFileSync(...)` calls from `child_process` | | [`require-spawnsync-error-check`](#require-spawnsync-error-check) | Require checking `result.error` after `spawnSync` calls | ### `no-github-request-interpolated-route` @@ -429,3 +431,39 @@ Prefer `@actions/core` logging methods (`core.info`, `core.debug`) over `console `console.error` and `console.warn` write to **`process.stderr`**, while `core.error` and `core.warning` emit GitHub Actions workflow commands to **`process.stdout`**. For processes that own stdout as a data/protocol channel — such as stdio MCP servers and transports — replacing stderr logging with stdout logging would corrupt the JSON-RPC stream. Because the stream change is not behavior-preserving, the rule never reports `console.error` or `console.warn` and offers no suggestion to replace them. + +### `require-execsync-try-catch` + +Require `execSync` calls sourced from `child_process` to be wrapped in `try/catch`. + +Why: `execSync` throws an `Error` containing child-process result fields (for example `status`, `signal`, `stdout`, `stderr`) when the child process exits with a non-zero status code or is killed by a signal. An unhandled throw crashes the action without surfacing a useful diagnostic. + +**Detected forms:** +- `const { execSync } = require("child_process"); execSync(...)` — destructured. +- `const cp = require("child_process"); cp.execSync(...)` — namespace access. +- `const run = cp.execSync; run(...)` — aliased via member expression. +- `import { execSync } from "child_process"; execSync(...)` — ESM named import. +- Both `"child_process"` and `"node:child_process"` specifiers are recognized. + +**Not flagged:** +- `execSync` from any module other than `child_process` / `node:child_process`. +- Calls already inside an enclosing `try { ... } catch { ... }` block. + +### `require-execfilesync-try-catch` + +Require `execFileSync` calls sourced from `child_process` to be wrapped in `try/catch`. + +Why: `execFileSync` has identical throw-on-failure semantics to `execSync` — it throws an `Error` containing child-process result fields (for example `status`, `signal`, `stdout`, `stderr`) when the child process exits with a non-zero status code or is killed by a signal. An unhandled throw crashes the action without surfacing a useful diagnostic. + +**Detected forms:** +- `const { execFileSync } = require("child_process"); execFileSync(...)` — destructured. +- `const cp = require("child_process"); cp.execFileSync(...)` — namespace access. +- `const run = cp.execFileSync; run(...)` — aliased via member expression. +- `import { execFileSync } from "child_process"; execFileSync(...)` — ESM named import. +- Both `"child_process"` and `"node:child_process"` specifiers are recognized. + +**Not flagged:** +- `execFileSync` from any module other than `child_process` / `node:child_process`. +- Calls already inside an enclosing `try { ... } catch { ... }` block. + +**Out of scope:** `execFile` (the async, callback-based sibling) is intentionally excluded. The async form accepts a callback and does not throw synchronously; errors are delivered through the callback or the returned `ChildProcess` event emitter, so a synchronous try/catch provides no protection. diff --git a/eslint-factory/eslint.config.cjs b/eslint-factory/eslint.config.cjs index fd278b8b237..55b01a63d4f 100644 --- a/eslint-factory/eslint.config.cjs +++ b/eslint-factory/eslint.config.cjs @@ -36,6 +36,7 @@ module.exports = [ "gh-aw-custom/no-core-error-then-process-exitcode": "warn", "gh-aw-custom/no-exec-interpolated-command": "warn", "gh-aw-custom/require-execsync-try-catch": "warn", + "gh-aw-custom/require-execfilesync-try-catch": "warn", "gh-aw-custom/require-fs-io-try-catch": "warn", "gh-aw-custom/no-setfailed-then-exit-zero": "warn", }, diff --git a/eslint-factory/src/index.ts b/eslint-factory/src/index.ts index 2e37862a201..1e50e9f3ac2 100644 --- a/eslint-factory/src/index.ts +++ b/eslint-factory/src/index.ts @@ -22,6 +22,7 @@ import { noCoreErrorThenProcessExitRule } from "./rules/no-core-error-then-proce import { noCoreErrorThenProcessExitCodeRule } from "./rules/no-core-error-then-process-exitcode"; import { noExecInterpolatedCommandRule } from "./rules/no-exec-interpolated-command"; import { requireExecSyncTryCatchRule } from "./rules/require-execsync-try-catch"; +import { requireExecFileSyncTryCatchRule } from "./rules/require-execfilesync-try-catch"; import { requireFsIoTryCatchRule } from "./rules/require-fs-io-try-catch"; import { noSetFailedThenExitZeroRule } from "./rules/no-setfailed-then-exit-zero"; @@ -55,6 +56,7 @@ const plugin = { "no-core-error-then-process-exitcode": noCoreErrorThenProcessExitCodeRule, "no-exec-interpolated-command": noExecInterpolatedCommandRule, "require-execsync-try-catch": requireExecSyncTryCatchRule, + "require-execfilesync-try-catch": requireExecFileSyncTryCatchRule, "require-fs-io-try-catch": requireFsIoTryCatchRule, "no-setfailed-then-exit-zero": noSetFailedThenExitZeroRule, }, diff --git a/eslint-factory/src/rules/require-execfilesync-try-catch.test.ts b/eslint-factory/src/rules/require-execfilesync-try-catch.test.ts new file mode 100644 index 00000000000..3302a2f6c49 --- /dev/null +++ b/eslint-factory/src/rules/require-execfilesync-try-catch.test.ts @@ -0,0 +1,182 @@ +import { RuleTester } from "eslint"; +import { describe, it } from "vitest"; +import { requireExecFileSyncTryCatchRule } from "./require-execfilesync-try-catch"; + +const cjsRuleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: "commonjs", + }, +}); + +const esmRuleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + }, +}); + +describe("require-execfilesync-try-catch", () => { + it("valid: execFileSync inside try block passes (CommonJS, destructured)", () => { + cjsRuleTester.run("require-execfilesync-try-catch", requireExecFileSyncTryCatchRule, { + valid: [ + `const { execFileSync } = require("child_process"); try { execFileSync("git", ["status"]); } catch (e) {}`, + `const { execFileSync } = require("node:child_process"); try { execFileSync("git", ["status"]); } catch (e) {}`, + `const { execFileSync } = require("child_process"); function f() { try { execFileSync("git", ["status"]); } catch (e) {} }`, + ], + invalid: [], + }); + }); + + it("valid: execFileSync inside try block passes (CommonJS, namespace)", () => { + cjsRuleTester.run("require-execfilesync-try-catch", requireExecFileSyncTryCatchRule, { + valid: [`const cp = require("child_process"); try { cp.execFileSync("git", ["status"]); } catch (e) {}`], + invalid: [], + }); + }); + + it("valid: execFileSync inside try block passes (ES module)", () => { + esmRuleTester.run("require-execfilesync-try-catch", requireExecFileSyncTryCatchRule, { + valid: [`import { execFileSync } from "child_process"; try { execFileSync("git", ["status"]); } catch (e) {}`, `import { execFileSync } from "node:child_process"; try { execFileSync("git", ["status"]); } catch (e) {}`], + invalid: [], + }); + }); + + it("valid: execFileSync from non-child_process module is ignored", () => { + cjsRuleTester.run("require-execfilesync-try-catch", requireExecFileSyncTryCatchRule, { + valid: [ + // execFileSync from an unrelated module — should not be flagged + `const { execFileSync } = require("some-other-lib"); execFileSync("git", ["status"]);`, + // bare execFileSync without any require — should not be flagged + `execFileSync("git", ["status"]);`, + // member call on unrelated object + `mockChild.execFileSync("git", ["status"]);`, + ], + invalid: [], + }); + }); + + it("invalid: execFileSync without try/catch (CommonJS, destructured)", () => { + cjsRuleTester.run("require-execfilesync-try-catch", requireExecFileSyncTryCatchRule, { + valid: [], + invalid: [ + { + code: `const { execFileSync } = require("child_process"); execFileSync("git", ["status"]);`, + errors: [ + { + messageId: "requireTryCatch", + suggestions: [ + { + messageId: "wrapInTryCatch", + output: `const { execFileSync } = require("child_process"); try {\n execFileSync("git", ["status"]);\n} catch (err) {\n // TODO: handle execFileSync failure (non-zero exit / signal termination).\n throw new Error(\n "execFileSync failed: " + (err instanceof Error ? err.message : String(err)),\n { cause: err },\n );\n}`, + }, + ], + }, + ], + }, + ], + }); + }); + + it("invalid: execFileSync without try/catch (CommonJS, namespace)", () => { + cjsRuleTester.run("require-execfilesync-try-catch", requireExecFileSyncTryCatchRule, { + valid: [], + invalid: [ + { + code: `const cp = require("child_process"); cp.execFileSync("git", ["status"]);`, + errors: [ + { + messageId: "requireTryCatch", + suggestions: [ + { + messageId: "wrapInTryCatch", + output: `const cp = require("child_process"); try {\n cp.execFileSync("git", ["status"]);\n} catch (err) {\n // TODO: handle execFileSync failure (non-zero exit / signal termination).\n throw new Error(\n "execFileSync failed: " + (err instanceof Error ? err.message : String(err)),\n { cause: err },\n );\n}`, + }, + ], + }, + ], + }, + { + code: `const cp = require("node:child_process"); cp.execFileSync("git", ["status"]);`, + errors: [ + { + messageId: "requireTryCatch", + suggestions: [ + { + messageId: "wrapInTryCatch", + output: `const cp = require("node:child_process"); try {\n cp.execFileSync("git", ["status"]);\n} catch (err) {\n // TODO: handle execFileSync failure (non-zero exit / signal termination).\n throw new Error(\n "execFileSync failed: " + (err instanceof Error ? err.message : String(err)),\n { cause: err },\n );\n}`, + }, + ], + }, + ], + }, + ], + }); + }); + + it("invalid: execFileSync without try/catch (ES module)", () => { + esmRuleTester.run("require-execfilesync-try-catch", requireExecFileSyncTryCatchRule, { + valid: [], + invalid: [ + { + code: `import { execFileSync } from "child_process"; execFileSync("git", ["status"]);`, + errors: [ + { + messageId: "requireTryCatch", + suggestions: [ + { + messageId: "wrapInTryCatch", + output: `import { execFileSync } from "child_process"; try {\n execFileSync("git", ["status"]);\n} catch (err) {\n // TODO: handle execFileSync failure (non-zero exit / signal termination).\n throw new Error(\n "execFileSync failed: " + (err instanceof Error ? err.message : String(err)),\n { cause: err },\n );\n}`, + }, + ], + }, + ], + }, + ], + }); + }); + + it("invalid: destructured alias of execFileSync without try/catch is flagged", () => { + cjsRuleTester.run("require-execfilesync-try-catch", requireExecFileSyncTryCatchRule, { + valid: [], + invalid: [ + { + code: `const { execFileSync: run } = require("child_process"); run("git", ["status"]);`, + errors: [ + { + messageId: "requireTryCatch", + suggestions: [ + { + messageId: "wrapInTryCatch", + output: `const { execFileSync: run } = require("child_process"); try {\n run("git", ["status"]);\n} catch (err) {\n // TODO: handle execFileSync failure (non-zero exit / signal termination).\n throw new Error(\n "execFileSync failed: " + (err instanceof Error ? err.message : String(err)),\n { cause: err },\n );\n}`, + }, + ], + }, + ], + }, + ], + }); + }); + + it("invalid: member-expression alias of execFileSync without try/catch is flagged", () => { + cjsRuleTester.run("require-execfilesync-try-catch", requireExecFileSyncTryCatchRule, { + valid: [], + invalid: [ + { + code: `const cp = require("child_process"); const run = cp.execFileSync; run("git", ["status"]);`, + errors: [ + { + messageId: "requireTryCatch", + suggestions: [ + { + messageId: "wrapInTryCatch", + output: `const cp = require("child_process"); const run = cp.execFileSync; try {\n run("git", ["status"]);\n} catch (err) {\n // TODO: handle execFileSync failure (non-zero exit / signal termination).\n throw new Error(\n "execFileSync failed: " + (err instanceof Error ? err.message : String(err)),\n { cause: err },\n );\n}`, + }, + ], + }, + ], + }, + ], + }); + }); +}); diff --git a/eslint-factory/src/rules/require-execfilesync-try-catch.ts b/eslint-factory/src/rules/require-execfilesync-try-catch.ts new file mode 100644 index 00000000000..58bb82f1003 --- /dev/null +++ b/eslint-factory/src/rules/require-execfilesync-try-catch.ts @@ -0,0 +1,135 @@ +import { AST_NODE_TYPES, ESLintUtils, TSESLint, TSESTree } from "@typescript-eslint/utils"; +import { buildTryCatchSuggestion, findEnclosingStatement, isChildProcessImportBinding, isChildProcessObjectBinding, isInsideTryBlock, isRequireChildProcess } from "./try-catch-rule-utils"; + +const createRule = ESLintUtils.RuleCreator(name => `https://github.com/github/gh-aw/tree/main/eslint-factory#${name}`); + +type SourceCodeScope = ReturnType; + +/** + * Walks the scope chain to decide whether `identifierName` resolves to + * `execFileSync` from `child_process`. + */ +function isExecFileSyncBinding(identifierName: string, scopeNode: TSESTree.Node, sourceCode: TSESLint.SourceCode): boolean { + let scope: SourceCodeScope | null = sourceCode.getScope(scopeNode); + while (scope) { + const variable = scope.set.get(identifierName); + if (variable && variable.defs.length > 0) { + for (const def of variable.defs) { + // ESM: import { execFileSync } from "child_process" + if (isChildProcessImportBinding(def) && def.node.type === AST_NODE_TYPES.ImportSpecifier) { + const specifier = def.node as TSESTree.ImportSpecifier; + const importedName = specifier.imported.type === AST_NODE_TYPES.Identifier ? specifier.imported.name : null; + if (importedName === "execFileSync") return true; + } + // CJS: const { execFileSync } = require("child_process") + if (def.type === "Variable") { + const declarator = def.node as TSESTree.VariableDeclarator; + if (declarator.id.type === AST_NODE_TYPES.ObjectPattern && isRequireChildProcess(declarator.init)) { + for (const prop of declarator.id.properties) { + if (prop.type !== AST_NODE_TYPES.Property) continue; + if (prop.key.type !== AST_NODE_TYPES.Identifier || prop.key.name !== "execFileSync") continue; + const boundName = prop.value.type === AST_NODE_TYPES.Identifier ? prop.value.name : null; + if (boundName === identifierName) return true; + } + } + // const execFileSync = childProcess.execFileSync + if (declarator.id.type === AST_NODE_TYPES.Identifier && declarator.init?.type === AST_NODE_TYPES.MemberExpression) { + const init = declarator.init; + if ( + !init.computed && + init.object.type === AST_NODE_TYPES.Identifier && + isChildProcessObjectBinding(init.object.name, init.object, sourceCode) && + init.property.type === AST_NODE_TYPES.Identifier && + init.property.name === "execFileSync" + ) { + return true; + } + } + } + } + return false; + } + scope = scope.upper; + } + return false; +} + +/** + * Returns true if the CallExpression is an `execFileSync(...)` call sourced from + * the `child_process` module. + */ +function isExecFileSyncCall(node: TSESTree.CallExpression, sourceCode: TSESLint.SourceCode): boolean { + const callee = node.callee; + + // execFileSync(...) — destructured or aliased + if (callee.type === AST_NODE_TYPES.Identifier) { + return isExecFileSyncBinding(callee.name, callee, sourceCode); + } + + // childProcess.execFileSync(...) or cp.execFileSync(...) + if (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 === "execFileSync") { + return isChildProcessObjectBinding(callee.object.name, callee.object, sourceCode); + } + + return false; +} + +export const requireExecFileSyncTryCatchRule = createRule({ + name: "require-execfilesync-try-catch", + meta: { + type: "problem", + hasSuggestions: true, + docs: { + description: + "Require execFileSync calls in actions/setup/js scripts to be wrapped in try/catch. " + + "execFileSync throws an Error containing child-process result fields when the child process exits with a non-zero status code or is killed by a signal; " + + "an unhandled throw crashes the action without surfacing a useful diagnostic.", + }, + schema: [], + messages: { + requireTryCatch: "Wrap execFileSync({{arg}}) in try/catch — execFileSync throws when the process exits non-zero or is killed by a signal, " + "and will crash the action if the error is unhandled.", + wrapInTryCatch: "Wrap in try { ... } catch { ... } and re-throw with { cause: err } to preserve context.", + }, + }, + defaultOptions: [], + create(context) { + const sourceCode = context.sourceCode; + + return { + CallExpression(node) { + if (!isExecFileSyncCall(node, sourceCode)) return; + if (isInsideTryBlock(sourceCode, node)) return; + + const argText = node.arguments.length > 0 ? sourceCode.getText(node.arguments[0]) : ""; + const stmt = findEnclosingStatement(sourceCode, node); + + context.report({ + node, + messageId: "requireTryCatch", + data: { arg: argText }, + suggest: stmt + ? [ + { + messageId: "wrapInTryCatch", + fix(fixer) { + const stmtText = sourceCode.getText(stmt); + const startLine = stmt.loc?.start.line; + const stmtLine = startLine !== undefined ? (sourceCode.lines[startLine - 1] ?? "") : ""; + const indent = stmtLine.match(/^(\s*)/)?.[1] ?? ""; + return fixer.replaceText( + stmt, + buildTryCatchSuggestion(stmtText, { + indent, + todoComment: "TODO: handle execFileSync failure (non-zero exit / signal termination).", + errorPrefix: "execFileSync failed: ", + }) + ); + }, + }, + ] + : [], + }); + }, + }; + }, +}); diff --git a/eslint-factory/src/rules/require-execsync-try-catch.ts b/eslint-factory/src/rules/require-execsync-try-catch.ts index fe0a8374484..268d08bb247 100644 --- a/eslint-factory/src/rules/require-execsync-try-catch.ts +++ b/eslint-factory/src/rules/require-execsync-try-catch.ts @@ -1,32 +1,10 @@ import { AST_NODE_TYPES, ESLintUtils, TSESLint, TSESTree } from "@typescript-eslint/utils"; -import { buildTryCatchSuggestion, findEnclosingStatement, isInsideTryBlock } from "./try-catch-rule-utils"; +import { buildTryCatchSuggestion, findEnclosingStatement, isChildProcessImportBinding, isChildProcessObjectBinding, isInsideTryBlock, isRequireChildProcess } from "./try-catch-rule-utils"; const createRule = ESLintUtils.RuleCreator(name => `https://github.com/github/gh-aw/tree/main/eslint-factory#${name}`); -const CHILD_PROCESS_SPECIFIERS = new Set(["child_process", "node:child_process"]); - type SourceCodeScope = ReturnType; -function isRequireChildProcess(node: TSESTree.Node | null | undefined): boolean { - if (!node) return false; - return ( - node.type === AST_NODE_TYPES.CallExpression && - node.callee.type === AST_NODE_TYPES.Identifier && - node.callee.name === "require" && - node.arguments.length >= 1 && - node.arguments[0].type === AST_NODE_TYPES.Literal && - typeof (node.arguments[0] as TSESTree.Literal).value === "string" && - CHILD_PROCESS_SPECIFIERS.has((node.arguments[0] as TSESTree.Literal).value as string) - ); -} - -function isChildProcessImportBinding(def: { type: string; node: TSESTree.Node; parent?: TSESTree.Node | null }): boolean { - if (def.type !== "ImportBinding") return false; - if (!def.parent || def.parent.type !== AST_NODE_TYPES.ImportDeclaration) return false; - if (def.parent.source.type !== AST_NODE_TYPES.Literal) return false; - return typeof def.parent.source.value === "string" && CHILD_PROCESS_SPECIFIERS.has(def.parent.source.value); -} - /** * Walks the scope chain to decide whether `identifierName` resolves to * `execSync` from `child_process`. @@ -76,29 +54,6 @@ function isExecSyncBinding(identifierName: string, scopeNode: TSESTree.Node, sou return false; } -function isChildProcessObjectBinding(name: string, scopeNode: TSESTree.Node, sourceCode: TSESLint.SourceCode): boolean { - let scope: SourceCodeScope | null = sourceCode.getScope(scopeNode); - while (scope) { - const variable = scope.set.get(name); - if (variable && variable.defs.length > 0) { - for (const def of variable.defs) { - if (def.type === "Variable") { - const declarator = def.node as TSESTree.VariableDeclarator; - if (declarator.id.type === AST_NODE_TYPES.Identifier && isRequireChildProcess(declarator.init)) { - return true; - } - } - if (isChildProcessImportBinding(def) && def.node.type === AST_NODE_TYPES.ImportNamespaceSpecifier) { - return true; - } - } - return false; - } - scope = scope.upper; - } - return false; -} - /** * Returns true if the CallExpression is an `execSync(...)` call sourced from * the `child_process` module. @@ -127,7 +82,7 @@ export const requireExecSyncTryCatchRule = createRule({ 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; " + + "execSync throws an Error containing child-process result fields when the child process exits with a non-zero status code or is killed by a signal; " + "an unhandled throw crashes the action without surfacing a useful diagnostic.", }, schema: [], diff --git a/eslint-factory/src/rules/try-catch-rule-utils.ts b/eslint-factory/src/rules/try-catch-rule-utils.ts index 089c8f2d9ba..2214f82bfc8 100644 --- a/eslint-factory/src/rules/try-catch-rule-utils.ts +++ b/eslint-factory/src/rules/try-catch-rule-utils.ts @@ -14,6 +14,8 @@ const FS_MODULE_SPECIFIERS = new Set(["fs", "node:fs"]); type SourceCodeScope = ReturnType; type FsBindingDefinition = { type: string; node: TSESTree.Node; parent?: TSESTree.Node | null }; +type ChildProcessBindingDefinition = { type: string; node: TSESTree.Node; parent?: TSESTree.Node | null }; +const CHILD_PROCESS_SPECIFIERS = new Set(["child_process", "node:child_process"]); function escapeRegex(text: string): string { return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -116,6 +118,49 @@ function isRequireFsCall(node: TSESTree.Node | null | undefined): boolean { ); } +export function isRequireChildProcess(node: TSESTree.Node | null | undefined): boolean { + if (!node) return false; + return ( + node.type === AST_NODE_TYPES.CallExpression && + node.callee.type === AST_NODE_TYPES.Identifier && + node.callee.name === "require" && + node.arguments.length >= 1 && + node.arguments[0].type === AST_NODE_TYPES.Literal && + typeof (node.arguments[0] as TSESTree.Literal).value === "string" && + CHILD_PROCESS_SPECIFIERS.has((node.arguments[0] as TSESTree.Literal).value as string) + ); +} + +export function isChildProcessImportBinding(definition: ChildProcessBindingDefinition): boolean { + if (definition.type !== "ImportBinding") return false; + if (!definition.parent || definition.parent.type !== AST_NODE_TYPES.ImportDeclaration) return false; + if (definition.parent.source.type !== AST_NODE_TYPES.Literal) return false; + return typeof definition.parent.source.value === "string" && CHILD_PROCESS_SPECIFIERS.has(definition.parent.source.value); +} + +export function isChildProcessObjectBinding(name: string, scopeNode: TSESTree.Node, sourceCode: TSESLint.SourceCode): boolean { + let scope: SourceCodeScope | null = sourceCode.getScope(scopeNode); + while (scope) { + const variable = scope.set.get(name); + if (variable && variable.defs.length > 0) { + for (const def of variable.defs) { + if (def.type === "Variable") { + const declarator = def.node as TSESTree.VariableDeclarator; + if (declarator.id.type === AST_NODE_TYPES.Identifier && isRequireChildProcess(declarator.init)) { + return true; + } + } + if (isChildProcessImportBinding(def) && def.node.type === AST_NODE_TYPES.ImportNamespaceSpecifier) { + return true; + } + } + return false; + } + scope = scope.upper; + } + return false; +} + function isFsImportBinding(definition: FsBindingDefinition): boolean { if (definition.type !== "ImportBinding") return false; if (!definition.parent || definition.parent.type !== AST_NODE_TYPES.ImportDeclaration) return false;