helpers/astHelpers.psm1
|
using namespace System.Management.Automation.Language Class AstHelpers { # Returns true only for a standalone, static Add-Type command that can be # executed before deferred PowerShell classes are compiled. # # This is intentionally conservative: # - nested commands keep their if/try/function execution context; # - commands participating in a larger pipeline stay with that pipeline; # - assignments, redirections, -PassThru and dynamic arguments stay in place. # # The same predicate is used by Replacer and FileInfo so a command cannot be # removed as "hoisted" by one component but counted as runtime code by another. [bool]IsHoistableAddType([CommandAst]$commandAst, [ScriptBlockAst]$rootAst, [bool]$deferClassesCompilation) { if (-not $deferClassesCompilation -or -not $commandAst -or -not $rootAst) { return $false } if ($commandAst.GetCommandName() -ne "Add-Type") { return $false } if ($commandAst.InvocationOperator -ne [TokenKind]::Unknown) { return $false } $pipeline = $commandAst.Parent # Removing only CommandAst from "Add-Type ... | Out-Null" would leave an # invalid "| Out-Null" fragment. We hoist the containing pipeline, but only # when Add-Type is its sole element. if ($pipeline -isnot [PipelineAst] -or $pipeline.PipelineElements.Count -ne 1) { return $false } # A direct file-level command has the following parents: # CommandAst -> PipelineAst -> NamedBlockAst -> root ScriptBlockAst. # Any if, try, loop, function or assignment inserts another AST node and # therefore prevents hoisting. if ($pipeline.Parent -isnot [NamedBlockAst] ` -or -not [object]::ReferenceEquals($pipeline.Parent.Parent, $rootAst)) { return $false } if ($commandAst.Redirections.Count -gt 0) { return $false } for ($i = 1; $i -lt $commandAst.CommandElements.Count; $i++) { $element = $commandAst.CommandElements[$i] if ($element -is [CommandParameterAst]) { # -PassThru makes command output observable at its original # position. Parameter abbreviations such as -Pass are rejected too. if ("PassThru".StartsWith($element.ParameterName, [StringComparison]::OrdinalIgnoreCase)) { return $false } if ($element.Argument -and -not $this.IsStaticExpression($element.Argument)) { return $false } continue } if (-not $this.IsStaticExpression($element)) { return $false } } return $true } # Static arguments can be moved without also moving variable assignments, # command substitutions or other prerequisite code. Expandable strings are # accepted only when they contain no nested expressions. [bool]IsStaticExpression([Ast]$ast) { if ($ast -is [StringConstantExpressionAst] -or $ast -is [ConstantExpressionAst]) { return $true } if ($ast -is [ExpandableStringExpressionAst]) { return -not $ast.NestedExpressions -or $ast.NestedExpressions.Count -eq 0 } if ($ast -is [ArrayLiteralAst]) { foreach ($element in $ast.Elements) { if (-not $this.IsStaticExpression($element)) { return $false } } return $true } return $false } # Returns CommandAst parameters names and values as Ast [hashtable[]]GetCommandAstParamsAst([CommandAst]$commandAst) { $result = @() $elements = $commandAst.CommandElements if ($elements.Count -lt 2) { return $result } # no parameters (first element is command name) # start from 1 to skip command name for ($i = 1; $i -lt $elements.Count; $i++) { $el = $elements[$i] if ($el -isnot [CommandParameterAst]) { $result += @{ name = "" value = $el } continue } $parName = $el.ParameterName $parValue = $null if ($i + 1 -lt $elements.Count -and $elements[$i + 1] -isnot [CommandParameterAst]) { $parValue = $elements[$i + 1] $i++ } $result += @{ name = $parName value = $parValue } } return $result } # Returns CommandAst parameters names and values map [System.Collections.Specialized.OrderedDictionary]GetNamedParametersMap([CommandAst]$commandAst) { $paramsList = $this.GetCommandAstParamsAst($commandAst) $result = [System.Collections.Specialized.OrderedDictionary]::new() foreach ($par in $paramsList) { if ($par.name) { $result[$par.name] = $par.value } } return $result } # Converts parameters map to string [string]ConvertParamsAstMapToString([System.Collections.Specialized.OrderedDictionary]$paramsMap) { $paramsStr = "" foreach ($key in $paramsMap.Keys) { $value = $paramsMap[$key] if ($value) { $paramsStr += " -$key " + $value.Extent.Text } else { $paramsStr += " -$key" } } return $paramsStr } } <# function Test-CommandParsing { param( [string] $CommandLine ) $astHelper = [AstHelpers]::new() $ast = [System.Management.Automation.Language.Parser]::ParseInput($CommandLine, [ref]$null, [ref]$null) $cmdAsts = $ast.Find({ param($n) $n -is [System.Management.Automation.Language.CommandAst] }, $true) foreach ($cmd in $cmdAsts) { $params = $astHelper.ParseImportModuleCommandAst($cmd) } } Test-CommandParsing "import-module -name 'C:\module1.psm1' -Force -DisableNameChecking" #> |