Search code examples
powershellabstract-syntax-treeextent

PowerShell AST Modification and Extents


I am currently trying to use the AST functionality introduced in PowerShell 3.0 to modify a ScriptBlock. My requirement is that all the parameters in the parameter block of the ScriptBlock get a [Parameter(Mandatory)] attribute.

Basically the code should modify this:

Param([string]$x)

Write-Host $x

to this:

Param([Parameter(Mandatory)][string]$x)

Write-Host $x

However, I ran into a problem when adding that new attribute, since it expects an IScriptExtent and I am not sure how I should create a new IScriptExtent.

How can I create a new script extent? What values can I use for the position? Do I have to change the position of all following extents?

I tried just reusing the extent of each parameter I am modifying, but unfortunately this does not seem to yield the results it should (e.g. when I am calling ToString on the modified ScriptBlock I don't see any changes).

My implementation so far is based on the ICustomAstVisitor found here.

The most important method looks like this:

public object VisitParameter(ParameterAst parameterAst)
{
   var newName = VisitElement(parameterAst.Name);

   var extent = // What to do here?

   var mandatoryArg = new AttributeAst(extent, new ReflectionTypeName(typeof (ParameterAttribute)),
        new ExpressionAst[0],
        new[] {new NamedAttributeArgumentAst(extent, "Mandatory", new ConstantExpressionAst(extent, true), true)});

   var newAttributes = new[] {mandatoryArg}.Concat(VisitElements(parameterAst.Attributes));
   var newDefaultValue = VisitElement(parameterAst.DefaultValue);
      return new ParameterAst(parameterAst.Extent, newName, newAttributes, newDefaultValue);
}

Solution

  • The script extent is used primarily for error reporting, but is also used for debugging (for example, setting a line breakpoint.)

    In general, the options for synthesized script (like your example) are:

    • reuse an existing ast, presumably near/related to the ast you're adding
    • use an empty ast (basically create instances of ScriptExtent and ScriptPosition with no file, empty line)
    • create a synthetic extent that aids in debugging somehow, maybe with some special content

    In your example, any of the above are suitable. The second option is the simplest. The third option is just a variant of the second, but you would set the content to something useful, e.g.

    <#Generated: [Parameter(Mandatory)] #>