enhance(#3624): local/no-exact-case-env-access — ratchet ADR-1703 onto production env reads (epic #3411 Phase 4) (#3976)
* enhance(#3624): local/no-exact-case-env-access — ratchet ADR-1703 onto production env reads (epic #3411 Phase 4) Extends ADR-1703's portability rule catalog with a second production-runtime rule: it flags an exact-case read of a Windows case-varying environment variable (PATH, PATHEXT, ComSpec, USERPROFILE, TEMP, TMP, APPDATA) off any receiver that is not process.env itself, matched via an env-shaped-receiver check to avoid colliding with ordinary `.path`-named properties elsewhere in the tree. Exports the seam's private `_envGet` as `envGet` so the rule's remediation message names a real helper, and fixes the one pre-existing violation the tightened rule found (`src/runtime-hooks-surface.cts`'s `env.APPDATA` read). Closes #3624 * fix(#3624): extractStaticName recognizes non-computed Literal destructuring keys; add missing accessor-call test case Review findings from the code-review + isolated-adversarial passes: - extractStaticName only matched non-computed Identifier keys, so a destructuring like `const { 'PATH': v } = opts.env;` (the issue's own I8 acceptance case) silently evaded the rule. Widened to accept a Literal key regardless of computed, which is safe for MemberExpression too (its non-computed property is always an Identifier by grammar). - Added the missing RuleTester valid case for "a case-insensitive accessor call" (envGet(env, 'PATH')) from the issue's Done-when checklist. * docs: backfill changeset PR number for #3624 (PR #3976) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -459,8 +459,9 @@ function normalizeNodePath(execPath: string, opts?: NodeNormOpts): string {
|
||||
candidates.push(`${fnmRoot}/aliases/default/node.exe`);
|
||||
candidates.push(`${fnmRoot}/aliases/default/bin/node`);
|
||||
}
|
||||
if (env.APPDATA) {
|
||||
candidates.push(`${normalizeRootDir(env.APPDATA)}/fnm/aliases/default/node.exe`);
|
||||
const appdata = shellCmdProjection.envGet(env, 'APPDATA');
|
||||
if (appdata) {
|
||||
candidates.push(`${normalizeRootDir(appdata)}/fnm/aliases/default/node.exe`);
|
||||
}
|
||||
for (const candidate of candidates) {
|
||||
if (candidate && existsSync(candidate)) return candidate;
|
||||
|
||||
@@ -715,8 +715,12 @@ function _isFile(candidate: string, requireExecutable = false, platform: string
|
||||
*
|
||||
* Exact match wins when present, so a caller who sets the canonical name pays
|
||||
* no scan.
|
||||
*
|
||||
* Exported so callers outside this seam can route an exact-case-sensitive env
|
||||
* read through it instead of accessing a non-`process.env` object directly
|
||||
* (see `local/no-exact-case-env-access`).
|
||||
*/
|
||||
function _envGet(env: NodeJS.ProcessEnv, name: string): string | undefined {
|
||||
export function envGet(env: NodeJS.ProcessEnv, name: string): string | undefined {
|
||||
const exact = env[name];
|
||||
if (exact !== undefined) return exact;
|
||||
const lower = name.toLowerCase();
|
||||
@@ -818,7 +822,7 @@ export function resolveExecutableBinary(
|
||||
return _isFile(name, requireExecutable, platform) ? name : null;
|
||||
}
|
||||
const env = opts.env ?? process.env;
|
||||
const rawPath = opts.pathOverride !== undefined ? opts.pathOverride : (_envGet(env, 'PATH') || '');
|
||||
const rawPath = opts.pathOverride !== undefined ? opts.pathOverride : (envGet(env, 'PATH') || '');
|
||||
const pathSegments = String(rawPath).split(path.delimiter).filter(Boolean);
|
||||
const segments = [...(opts.prependPaths ?? []), ...pathSegments];
|
||||
|
||||
@@ -830,7 +834,7 @@ export function resolveExecutableBinary(
|
||||
return null;
|
||||
}
|
||||
|
||||
const exts = String(_envGet(env, 'PATHEXT') || DEFAULT_PATHEXT).split(';').filter(Boolean);
|
||||
const exts = String(envGet(env, 'PATHEXT') || DEFAULT_PATHEXT).split(';').filter(Boolean);
|
||||
// A name already ending in a PATHEXT-listed extension is an address, not a stem:
|
||||
// probing `foo.exe` as `foo.exe.EXE` would miss the file sitting right there.
|
||||
// Compared case-insensitively because PATHEXT casing is not guaranteed.
|
||||
@@ -935,7 +939,7 @@ export function projectSpawnInvocation(
|
||||
return resolved ? { command: resolved, args, resolved } : { command, args, resolved: null };
|
||||
}
|
||||
return {
|
||||
command: String(_envGet(env, 'ComSpec') || 'cmd.exe'),
|
||||
command: String(envGet(env, 'ComSpec') || 'cmd.exe'),
|
||||
args: ['/d', '/s', '/c', _buildVerbatimCmdLine(target, args)],
|
||||
resolved,
|
||||
windowsVerbatimArguments: true,
|
||||
|
||||
Reference in New Issue
Block a user