fix(powershell): extract functions inside file-scope begin/process/end blocks (#4270)

File-scope begin/process/end/dynamicparam/clean parse as pseudo-commands
(command_name 'begin'), not named_block, so the command branch returned before
the child walk and any function/class defined inside was dropped. Descend into
those five block keywords' children; ids stay name-based and stable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
safishamsiandClaude Opus 4.8 committed 2026-10-10 01:01:10 +01:00
1 parent cf5978e4fb
commit da287edfb5
2 files changed
+38

No files matched your search

+14
View File
@@ -61,6 +61,10 @@ def extract_powershell(path: Path) -> dict:
# Import commands — handled as import edges, not function calls
"import-module",
})
# File-scope advanced-function blocks that parse as pseudo-commands rather
# than named_block nodes; walk() must descend into them to reach nested
# function/class/enum statements (#4270).
_PS_NAMED_BLOCK_CMDS = frozenset({"begin", "process", "end", "dynamicparam", "clean"})
def _find_script_block_body(node):
for child in node.children:
@@ -314,6 +318,16 @@ def extract_powershell(path: Path) -> dict:
if bare:
add_edge(file_nid, _make_id(bare), "imports_from",
node.start_point[0] + 1)
elif cmd_text in _PS_NAMED_BLOCK_CMDS:
# File-scope begin/process/end/dynamicparam/clean blocks parse as a
# pseudo-command (command_name 'begin') whose `{ }` body is a
# script_block_expression under command_elements, NOT a real
# named_block (those exist only inside a function/scriptblock body).
# Descend so nested function/class/enum statements are extracted
# (#4270); without this the command branch returns before the
# generic child walk below and the whole block is dropped.
for child in node.children:
walk(child, parent_class_nid)
return
for child in node.children:
+24
View File
@@ -3078,6 +3078,30 @@ def test_powershell_psd1_no_dangling_edges():
assert e["source"] in node_ids, f"Dangling source in edge: {e}"
def test_powershell_functions_in_file_scope_begin_block_are_extracted(tmp_path):
# File-scope begin/process/end blocks parse as pseudo-commands (command_name
# 'begin'), not named_block, so functions defined inside them were dropped (#4270).
f = tmp_path / "advanced.ps1"
f.write_text(
"[CmdletBinding()]\n"
"param()\n"
"begin {\n"
" function Get-Foo { Get-Bar }\n"
" function Get-Bar { 'bar' }\n"
"}\n"
"process {\n"
" Get-Foo\n"
"}\n",
encoding="utf-8",
)
r = extract_powershell(f)
assert "error" not in r
labels = {n["label"] for n in r["nodes"]}
assert "Get-Foo()" in labels and "Get-Bar()" in labels
# the call between the two helpers (inside Get-Foo's body) is recovered
assert ("Get-Foo", "Get-Bar") in _edge_labels(r, "calls")
# ── TypeScript dynamic imports ───────────────────────────────────────────────
def test_ts_dynamic_import_no_error():