fix: distinguish PowerShell foreach statements

This commit is contained in:
Dante
2026-09-23 00:52:28 +08:00
parent bf70150eb2
commit 61c230c163
2 changed files with 42 additions and 11 deletions
+17 -11
View File
@@ -423,18 +423,21 @@ function currentClause(prefix) {
return prefix.slice(clauseStart + 1).trim();
}
function invokesContainerResult(prefix) {
function invokesContainerResult(prefix, options = {}) {
const clause = currentClause(prefix);
const pipelineStart = clause.lastIndexOf('|');
const pipelineCommand = clause.slice(pipelineStart + 1).trim();
const isForeachLoopHeader = Boolean(options.groupingExpression) &&
/^foreach$/i.test(pipelineCommand);
return /(?:^|\s)(?:&|\.)\s*$/.test(clause) ||
/\.\s*(?:foreach|where)\s*$/i.test(clause) ||
/-(?:action|begin|command|end|expression|filter|initializationscript|parallel|process|scriptblock)(?:\s*:\s*)?$/i.test(clause) ||
/^(?:(?:[\w.-]+\\)?(?:foreach-object|where-object|foreach|where|invoke-command|start-job|measure-command)|%|\?)(?:\s|$)/i.test(pipelineCommand);
(!isForeachLoopHeader &&
/^(?:(?:[\w.-]+\\)?(?:foreach-object|where-object|foreach|where|invoke-command|start-job|measure-command)|%|\?)(?:\s|$)/i.test(pipelineCommand));
}
function invokesDynamicResult(prefix) {
return invokesContainerResult(prefix) ||
function invokesDynamicResult(prefix, options = {}) {
return invokesContainerResult(prefix, options) ||
/(?:^|\s)(?:iex|invoke-expression)\s*$/i.test(currentClause(prefix));
}
@@ -850,6 +853,9 @@ function extractExecutableContainers(input, options = {}) {
const withinDoubleQuote = quote === '"';
const prefix = context;
const invokesContainer = invokesContainerResult(prefix, {
groupingExpression: isGroupingExpression,
});
const invokedAfter = isInvokedAfterContainer(input, group.end);
const createsScriptBlock = /\[\s*(?:system\.management\.automation\.)?scriptblock\s*\]\s*::\s*create\s*$/i.test(
currentClause(prefix)
@@ -863,7 +869,7 @@ function extractExecutableContainers(input, options = {}) {
options: {
executeBareScriptBlocks: Boolean(options.executeBareScriptBlocks) ||
invokedAfter || executesNestedScriptBlocks ||
(!isScriptBlock && invokesContainerResult(prefix)),
(!isScriptBlock && invokesContainer),
},
});
} else {
@@ -883,7 +889,7 @@ function extractExecutableContainers(input, options = {}) {
}
let resolvedCommand = null;
if (!isScriptBlock) {
if (isSubexpression || invokesContainerResult(prefix)) {
if (isSubexpression || invokesContainer) {
resolvedCommand = staticOutputResult(group.body);
if (resolvedCommand === null && isSubexpression) {
const scalarReference = variableReference(group.body);
@@ -904,16 +910,16 @@ function extractExecutableContainers(input, options = {}) {
const executableBlockExpression = /\{|\[\s*(?:system\.management\.automation\.)?scriptblock\s*\]\s*::\s*create/i.test(
maskQuotedStrings(group.body)
);
if (!resolvedCommand && !isScriptBlock && invokesDynamicResult(prefix) && !executableBlockExpression) {
if (!resolvedCommand && !isScriptBlock && invokesDynamicResult(prefix, {
groupingExpression: isGroupingExpression,
}) && !executableBlockExpression) {
resolvedCommand = DYNAMIC_EXECUTION_MARKER;
}
if (resolvedCommand) {
for (let offset = 0; offset < resolvedCommand.length; offset += 1) {
masked[index + offset] = resolvedCommand[offset];
}
masked[index] = resolvedCommand;
if (!withinDoubleQuote) appendContext(resolvedCommand);
} else if (isScriptBlock) {
if (invokesContainerResult(prefix)) {
if (invokesContainer) {
context = prefix;
} else {
resetContext();
@@ -472,6 +472,24 @@ test('classifies invoked functions and filters across executable containers', ()
for (const command of commands) expectRules(command, [RULES.REMOVE_FORCE]);
});
test('distinguishes foreach statements from the pipeline alias', () => {
for (const command of [
"foreach ($r in 'aaaaaaaaaaaaa') { $r }",
"foreach ($r in 'aaaaaaaaaaaaaa') { $r }",
"foreach ($s in 'BTCUSDT','ETHUSDT','SOLUSDT') { $s }",
]) {
expectSafe(command);
}
expectRules(
"foreach ($r in 'aaaaaaaaaaaaaa') { Remove-Item -Force C:/tmp/demo }",
[RULES.REMOVE_FORCE]
);
expectRules('1 | foreach { Remove-Item -Force C:/tmp/demo }', [
RULES.REMOVE_FORCE,
]);
});
test('classifies invoked static script-block variables but leaves assignments inert', () => {
expectSafe('$cleanup = { Remove-Item -Force C:/tmp/demo }');
expectRules('$cleanup = { Remove-Item -Force C:/tmp/demo }; & $cleanup', [
@@ -620,6 +638,13 @@ test('classifies static execution primitives', () => {
expectRules('& (Get-Command Remove-Item) -Force C:/tmp/demo', [
RULES.DYNAMIC_EXECUTION,
]);
for (const command of [
'iex ($a); Remove-Item -Force C:/tmp/demo',
'Invoke-Expression ($a); Remove-Item -Force C:/tmp/demo',
'& ($a); Remove-Item -Force C:/tmp/demo',
]) {
expectRules(command, [RULES.DYNAMIC_EXECUTION, RULES.REMOVE_FORCE]);
}
expectRules("iex ('Remove-'+'Item -Force C:/tmp/demo')", [
RULES.DYNAMIC_EXECUTION,
]);