From 679c4e095a5395a30f501122dd906240fa791346 Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Fri, 23 Oct 2026 11:35:14 -0500 Subject: [PATCH] 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 --- __tests__/child-process-windows-hide.test.ts | 21 +++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/__tests__/child-process-windows-hide.test.ts b/__tests__/child-process-windows-hide.test.ts index 0e969934..5f8ba888 100644 --- a/__tests__/child-process-windows-hide.test.ts +++ b/__tests__/child-process-windows-hide.test.ts @@ -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: '.' });`],