Skip to content

Add GetScriptBlock to FunctionDefinitionAst - #28200

Open
Jordan Borean (jborean93) wants to merge 2 commits into
PowerShell:masterfrom
jborean93:fda-getscriptblock
Open

Jordan Borean (jborean93) wants to merge 2 commits into
PowerShell:masterfrom
jborean93:fda-getscriptblock

Conversation

@jborean93

Copy link
Copy Markdown
Collaborator

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/$PSScriptRoot won'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:

# 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.

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

`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.
Copilot AI balanced review requested due to automatic review settings October 14, 2026 03:22
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment thread src/System.Management.Automation/engine/parser/ast.cs
@kilasuit Ryan Yates (kilasuit) added WG-Engine core PowerShell engine, interpreter, and runtime WG-NeedsReview Needs a review by the labeled Working Group CL-Engine Indicates that a PR should be marked as an engine change in the Change Log labels Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-Engine Indicates that a PR should be marked as an engine change in the Change Log WG-Engine core PowerShell engine, interpreter, and runtime WG-NeedsReview Needs a review by the labeled Working Group

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants