From b989db9c2950c9dbac3310d035099fc4f5ebc37c Mon Sep 17 00:00:00 2001 From: Sergei Romanov Date: Wed, 2 Sep 2026 15:14:21 +1000 Subject: [PATCH] test(vscode): cover object-literal captures in the guard `const ops = { exists: fs.existsSync }` captures a function reference at module load the same way the array-literal form does, and is the more natural thing to write. Same descendant selector shape, keyed on the watched namespaces; two banned fixtures (plain and exported-with-optional-chain) and two innocent ones, including a default parameter, which is evaluated per call and must stay legal. --- vscode/eslint.config.mjs | 7 +++++++ vscode/scripts/lint-guard-self-test.sh | 20 +++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/vscode/eslint.config.mjs b/vscode/eslint.config.mjs index 359f08a..fce0ab3 100644 --- a/vscode/eslint.config.mjs +++ b/vscode/eslint.config.mjs @@ -88,6 +88,13 @@ const captureBans = [ selector: `Program > ExportNamedDeclaration > VariableDeclaration > VariableDeclarator[id.type='ArrayPattern'] > ArrayExpression MemberExpression${selfRootedInNs}`, message: CAPTURE_MSG, }, + // Object literal: `const ops = { exists: fs.existsSync }` captures the same + // way through a property value; same descendant shape as the array case. + { selector: `Program > VariableDeclaration > VariableDeclarator > ObjectExpression MemberExpression${selfRootedInNs}`, message: CAPTURE_MSG }, + { + selector: `Program > ExportNamedDeclaration > VariableDeclaration > VariableDeclarator > ObjectExpression MemberExpression${selfRootedInNs}`, + message: CAPTURE_MSG, + }, ]; export default tseslint.config( diff --git a/vscode/scripts/lint-guard-self-test.sh b/vscode/scripts/lint-guard-self-test.sh index 211e6a6..249ff74 100755 --- a/vscode/scripts/lint-guard-self-test.sh +++ b/vscode/scripts/lint-guard-self-test.sh @@ -6,7 +6,7 @@ # eslint.config.mjs forbids, in src/, every way of capturing an fs/child_process # function at module scope (named/default imports, a renamed namespace, # destructured require, top-level aliases, assignments, class fields, optional -# chains, array patterns): the +# chains, array and object literals): the # activation spies patch the module objects, and a captured function would # slip past them while the positive control stayed green. The guard is keyed # on the canonical namespace names (fs, fsp, cp), so innocent aliases of other @@ -176,6 +176,15 @@ expect_fail "array pattern: const [f] = [fs.existsSync]" \ const [f] = [fs.existsSync]; export { f };' +expect_fail "object literal: const ops = { exists: fs.existsSync }" \ +'import * as fs from "node:fs"; +const ops = { exists: fs.existsSync, read: fs.readFileSync }; +export { ops };' + +expect_fail "exported object literal with an optional chain: export const ops = { spawn: cp?.spawn }" \ +'import * as cp from "node:child_process"; +export const ops = { spawn: cp?.spawn };' + # ── allowed: the canonical form, and innocent aliases of other modules ────── expect_pass "namespace imports looked up at call time (fs, cp), in-function require alias" \ @@ -220,6 +229,15 @@ expect_pass "innocent array pattern: const [a] = [path.sep]" \ const [a] = [path.sep]; export { a };' +expect_pass "innocent object literal: const o = { sep: path.sep }" \ +'import * as path from "node:path"; +const o = { sep: path.sep }; +export { o };' + +expect_pass "default parameter evaluated per call: (h = fs.existsSync) => h" \ +'import * as fs from "node:fs"; +export const g = (h = fs.existsSync): unknown => h;' + if [ "$failed" -ne 0 ]; then echo "✖ lint guard self-test: a fixture misbehaved" >&2 exit 1