test(windows): the windowsHide guard reads quoted and computed keys, and named options

The static guard in child-process-windows-hide.test.ts, which checks that
every spawn under src/ sets windowsHide: true, matched the key by its
source text. A quoted key was a different property to it, so
{ windowsHide: true, 'windowsHide': false } passed as hidden and
{ 'windowsHide': true } was flagged. A computed key written after
windowsHide: true, like [key]: false, was skipped, so it passed too. And
(file, options) with the args array omitted was recognized only when the
options were an object literal, so execFileSync('git', opts) was read as
passing no options and flagged.

A quoted key is now the same property, a computed key makes the options
unprovable from that point on, and with exactly two arguments the second
one is what gets checked, so an args array there is still an offender.

Validation: with main's guard, exactly the 4 new table rows fail and the
other 29 pass; with this change all 33 pass, including the scan of src/,
which flags nothing on either side. Test-only, so no CHANGELOG entry.

Original PR #2371 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
danusha2345
2026-10-23 16:38:59 +00:00
committed by Colby Mchenry
co-authored by Claude Opus 5.5
parent 9c4794d694
commit 679c4e095a
+14 -7
View File
@@ -117,8 +117,9 @@ function spawnerOf(call: ts.CallExpression, b: ChildProcessBindings): string | n
function optionsArg(call: ts.CallExpression, spawner: string): ts.Expression | undefined {
const [, second, third] = call.arguments;
if (SPAWNERS[spawner] === 'command') return second;
// (file, options) is allowed when the args array is omitted.
if (second && ts.isObjectLiteralExpression(second)) return second;
// (file, options) is allowed when the args array is omitted: an object there
// is the options, literal or not (`execFileSync('git', opts)`).
if (call.arguments.length === 2 || (second && ts.isObjectLiteralExpression(second))) return second;
return third;
}
@@ -215,13 +216,15 @@ function hideState(expr: ts.Expression | undefined, sf: ts.SourceFile, seen = ne
if (ts.isObjectLiteralExpression(e)) {
let state: HideState = 'absent';
for (const prop of e.properties) {
if (ts.isPropertyAssignment(prop) && prop.name.getText(sf) === 'windowsHide') {
state = unwrap(prop.initializer).kind === ts.SyntaxKind.TrueKeyword ? 'hidden' : 'unhidden';
} else if (ts.isShorthandPropertyAssignment(prop) && prop.name.text === 'windowsHide') {
state = 'unhidden'; // not provable statically
} else if (ts.isSpreadAssignment(prop)) {
if (ts.isSpreadAssignment(prop)) {
const spread = hideState(prop.expression, sf, trail);
if (spread !== 'absent') state = spread;
} else if (ts.isComputedPropertyName(prop.name)) {
state = 'unhidden'; // `[key]: …` may be windowsHide
} else if (prop.name.text === 'windowsHide') {
// Quoted or not, the same property. Only a literal `true` is provable:
// a shorthand, a method or an accessor is not.
state = ts.isPropertyAssignment(prop) && unwrap(prop.initializer).kind === ts.SyntaxKind.TrueKeyword ? 'hidden' : 'unhidden';
}
}
return state; // last write wins, matching object-literal semantics
@@ -330,6 +333,8 @@ describe('the windowsHide guard fails closed', () => {
['windowsHide: false', `import { spawn } from 'child_process'; spawn('git', [], { windowsHide: false });`],
['a later spread overriding it', `import { spawn } from 'child_process'; spawn('git', [], { windowsHide: true, ...{ windowsHide: false } });`],
['a later spread that might override it', `import { spawn } from 'child_process'; spawn('git', [], { windowsHide: true, ...extra() });`],
['a later quoted key overriding it', `import { spawn } from 'child_process'; spawn('git', [], { windowsHide: true, 'windowsHide': false });`],
['a later computed key that might override it', `import { spawn } from 'child_process'; spawn('git', [], { windowsHide: true, [key]: false });`],
['a default import', `import cp from 'child_process'; cp.execFileSync('git', ['status'], { encoding: 'utf8' });`],
['an import-equals', `import cp = require('child_process'); cp.spawnSync('git', ['status']);`],
['a require()', `const { execFileSync } = require('child_process'); execFileSync('git', ['status'], { windowsHide: true });`],
@@ -371,6 +376,8 @@ describe('the windowsHide guard fails closed', () => {
it.each([
['a literal', `import { execFileSync } from 'child_process'; execFileSync('git', ['status'], { windowsHide: true });`],
['(file, options)', `import { execFileSync } from 'child_process'; execFileSync('git', { windowsHide: true });`],
['(file, options) through a const', `import { execFileSync } from 'child_process'; const o = { windowsHide: true }; execFileSync('git', o);`],
['a quoted key', `import { spawn } from 'child_process'; spawn('git', [], { 'windowsHide': true });`],
['a command spawner', `import { exec } from 'child_process'; exec('git status', { windowsHide: true }); /a/.exec('a');`],
['a const', `import { spawn } from 'child_process'; const o = { windowsHide: true }; spawn('git', [], o);`],
['a spread const', `import { spawn } from 'child_process'; const base = { windowsHide: true }; spawn('git', [], { ...base, cwd: '.' });`],