Repository navigation
Add GetScriptBlock to FunctionDefinitionAst - #28200
Open
Jordan Borean (jborean93) wants to merge 2 commits into
Open
Jordan Borean (jborean93) wants to merge 2 commits into
Jordan Borean (jborean93) wants to merge 2 commits into
Conversation
`ScriptBlockAst.GetScriptBlock()` is the public way to get a new script
block from an ast that is not tied to the session state the original
was created in. This lets a module take a script block from the caller
and run it somewhere else, such as another runspace or a callback on a
different thread, without it reaching back into the caller's session
state.
This doesn't work when the caller passes a function's script block,
for example ${function:Foo} or (Get-Command Foo).ScriptBlock. Its Ast
is a FunctionDefinitionAst, which has no GetScriptBlock() method. The
only public workaround is Ast.Body.GetScriptBlock(). That drops the
parameters declared after the function name and the filter keyword,
so the result behaves differently from the function. Code that needs
the full function has to use reflection to reach the internal
ScriptBlock(IParameterMetadataProvider, bool) constructor.
For example, a module that runs user script blocks in its own runspace
can now handle both kinds of input in the same way with this PR:
# Works today
$sbk = { param($Prefix) "$Prefix $value" }
$value = 'caller'
$ps = [PowerShell]::Create()
$null = $ps.AddScript('param ($s) $value = "other"; & ($s.Ast.GetScriptBlock()) -Prefix V').
AddArgument($sbk)
$ps.Invoke() # V other
# PR allows this to work
function Get-Value ($Prefix) { "$Prefix $value" }
$value = 'caller'
$ps = [PowerShell]::Create()
$null = $ps.AddScript('param ($s) $value = "other"; & ($s.Ast.GetScriptBlock()) -Prefix V').
AddArgument(${function:Get-Value})
$ps.Invoke() # V other
Without unbinding, passing ${function:Get-Value} directly outputs
"V caller". That script block runs against the original runspace's
session state from another thread, which is not safe.
This change adds FunctionDefinitionAst.GetScriptBlock(). Like the
ScriptBlockAst version, it runs the post parse checks if they have not
been done and throws if the ast has errors. It then returns a new
unbound script block built from the whole function definition. The
result keeps the parameters declared after the function name and the
filter behaviour, matching how the engine builds the script block when
it defines the function.
The returned script block takes the current language mode, the same as
ScriptBlockAst.GetScriptBlock(). Ast.Body.GetScriptBlock() was already
public, so this gives constrained script nothing new and is still up to
the caller to validate and enforce properly.
Jordan Borean (jborean93)
requested review from
a team,
Patrick Meinecke (SeeminglyScience) and
Dongbo Wang (daxian-dbw)
as code owners
October 14, 2026 03:22
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The public exception documentation contradicts the implemented and tested behavior.
1 open finding
What changed in this PR
Adds FunctionDefinitionAst.GetScriptBlock() to create unbound script blocks while preserving function parameters, filters, and source information.
Changes:
- Adds validation and script-block creation for function-definition ASTs.
- Adds coverage for parameters, filters, runspaces, copied ASTs, and errors.
| File | Description |
|---|---|
src/System.Management.Automation/engine/parser/ast.cs |
Implements the new public API and shared validation. |
test/powershell/Language/Parser/LanguageAndParser.TestFollowup.Tests.ps1 |
Tests behavior and error handling. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

PR Summary
ScriptBlockAst.GetScriptBlock()is the public way to get a new script block from an ast that is not tied to the session state the original was created in. This lets a module take a script block from the caller and run it somewhere else, such as another runspace or a callback on a different thread, without it reaching back into the caller's session state.This doesn't work when the caller passes a function's script block, for example
${function:Foo}or(Get-Command Foo).ScriptBlock. Its Ast is a FunctionDefinitionAst, which has no GetScriptBlock() method. The only public workaround is Ast.Body.GetScriptBlock() or creating a new ScriptBlock from the Extent string. The first method drops the parameters declared after the function name and the filter keyword, so the result behaves differently from the function. The second works but strips the Ast file information so things like error messages no longer have a file position and$PSCommandPath/$PSScriptRootwon't work anymore. Code that needs the full function has to use reflection to reach the internal ScriptBlock(IParameterMetadataProvider, bool) constructor.For example, a module that runs user script blocks in its own runspace can now handle both kinds of input in the same way with this PR:
Without unbinding, passing ${function:Get-Value} directly outputs "V caller". That script block runs against the original runspace's session state from another thread, which is not safe.
PR Context
This change adds FunctionDefinitionAst.GetScriptBlock(). Like the ScriptBlockAst version, it runs the post parse checks if they have not been done and throws if the ast has errors. It then returns a new unbound script block built from the whole function definition. The result keeps the parameters declared after the function name and the filter behaviour, matching how the engine builds the script block when it defines the function.
The returned script block takes the current language mode, the same as ScriptBlockAst.GetScriptBlock(). Ast.Body.GetScriptBlock() was already public, so this gives constrained script nothing new and is still up to the caller to validate and enforce properly.
PR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright header