PowerShell AST Modification and Extents
Asked Answered
U

2

6

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);
}
Unanimous answered 14/3, 2016 at 17:17 Comment(0)
H
3

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)] #>
Hebdomadary answered 16/3, 2016 at 15:34 Comment(6)
Interesting. Though one problem I have is, that calling ToString() on the modified AST returns the old code and my thinking was that that happens because of the old extents. Is there another good way to turn the AST back into source code that doesn't rely on the extents being correct?Unanimous
Not that easily - a pretty printer is a perfectly reasonable thing to implement based on the Ast, but I'm not aware of one, other than some sample code I wrote a long time ago which actually doesn't do a very good job formatting.Hebdomadary
Well the "pretty printer" is the whole problem: I don't even need the code to be pretty, I just need to get the code AFTER modifying the AST and this does not seem to work without providing "correct" extents. In a way it even seems like it would be easier to just use string modification to modify the code instead of dabbling with the AST which would be quite a shame.Unanimous
@Unanimous Did you manage to find a solution? I currently have the same predicament.Stinkhorn
@Stinkhorn No, unfortunately I didn't.Unanimous
@Unanimous I'm not sure of what use it is now, but I've been pointed in the direction of code.msdn.microsoft.com/Script-Line-Profiler-Sample-80380291 - part of the PowerShell v3 SDK that inserts callbacks before each statement.Stinkhorn
D
3

Names that begin with I are typically Interfaces. They are not classes that you create an instance of, they are contracts of sorts that specify that a particular class implements a certain known set of functionality.

For example, a [hashtable] implements IEnumerable. That means that anything that knows how to work with an IEnumerable interface and operate on that class; you could create your own class that implements the interface, and code that never could have known about your class or what it does can still interact with it in the way that IEnumerable defines (which in this case is a way to iterate over it).

So, when a function declares a parameter with an interface type, it's not looking for any one specific class, it's looking for any class that implements that interface.

The next step then is to find which types implement that interface. Here's some PowerShell code I used to find those:

[System.AppDomain]::CurrentDomain.GetAssemblies().GetTypes() | Where-Object { 
    [System.Management.Automation.Language.IScriptExtent].IsAssignableFrom($_) 
}

From this, we can see the following:

IsPublic IsSerial Name                                     BaseType                                                    
-------- -------- ----                                     --------                                                    
True     False    IScriptExtent                                                                                        
False    False    InternalScriptExtent                     System.Object                                               
False    False    EmptyScriptExtent                        System.Object                                               
True     False    ScriptExtent                             System.Object                                               

The first listing is the interface itself. Of the other three, two of them are not public, so that just leaves ScriptExtent.

You can create one of these with New-Object but you need to supply the start and end positions as [ScriptPosition] objects. I'm not entirely sure what those should be without seeing more of your code.

Dextrin answered 14/3, 2016 at 18:34 Comment(2)
I know what an interface is, my question revolves around me not quite being able to figure out how extents work in PowerShell when creating new code (there are plenty of examples of people modifying code and reusing extents, but I couldn't find any example where people created new code).Unanimous
@Unanimous it's not quite clear from your question that you're familiar with interfaces since you ask how to create a new IScriptExtent so I felt it was best to err on the safe side and explain, especially since it may be helpful to other visitors who find your question but don't know what an interface is. You might also consider including in your question what you've tried so far.Dextrin
H
3

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)] #>
Hebdomadary answered 16/3, 2016 at 15:34 Comment(6)
Interesting. Though one problem I have is, that calling ToString() on the modified AST returns the old code and my thinking was that that happens because of the old extents. Is there another good way to turn the AST back into source code that doesn't rely on the extents being correct?Unanimous
Not that easily - a pretty printer is a perfectly reasonable thing to implement based on the Ast, but I'm not aware of one, other than some sample code I wrote a long time ago which actually doesn't do a very good job formatting.Hebdomadary
Well the "pretty printer" is the whole problem: I don't even need the code to be pretty, I just need to get the code AFTER modifying the AST and this does not seem to work without providing "correct" extents. In a way it even seems like it would be easier to just use string modification to modify the code instead of dabbling with the AST which would be quite a shame.Unanimous
@Unanimous Did you manage to find a solution? I currently have the same predicament.Stinkhorn
@Stinkhorn No, unfortunately I didn't.Unanimous
@Unanimous I'm not sure of what use it is now, but I've been pointed in the direction of code.msdn.microsoft.com/Script-Line-Profiler-Sample-80380291 - part of the PowerShell v3 SDK that inserts callbacks before each statement.Stinkhorn

© 2022 - 2024 — McMap. All rights reserved.