Private/Find-IslAstNode.ps1
|
# AST helpers shared by the rules. Type names are compared as strings so the module loads on # Windows PowerShell 5.1, where the PowerShell 7 node types (ternary, pipeline chain) don't exist. function Find-IslAstNode { <# .SYNOPSIS Finds AST nodes by type name, optionally filtered by a predicate. #> [CmdletBinding()] # The parameters are used inside the FindAll predicate; the analyzer can't see through it [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] param( [Parameter(Mandatory)] [System.Management.Automation.Language.Ast]$Ast, # One or more AST class names, e.g. 'CommandAst', 'ExitStatementAst' [Parameter(Mandatory)] [string[]]$TypeName, [scriptblock]$Where ) $Ast.FindAll({ param($node) if ($node.GetType().Name -notin $TypeName) { return $false } if ($Where) { return [bool](& $Where $node) } $true }, $true) } function Find-IslCommand { <# .SYNOPSIS Finds command invocations by name (case-insensitive), including aliases the caller lists. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] param( [Parameter(Mandatory)] [System.Management.Automation.Language.Ast]$Ast, [Parameter(Mandatory)] [string[]]$Name ) Find-IslAstNode -Ast $Ast -TypeName CommandAst -Where { param($node) $commandName = $node.GetCommandName() $commandName -and $commandName -in $Name } } function Test-IslCommandParameter { <# .SYNOPSIS True when the command is called with the named parameter (prefix match, like PowerShell). #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Management.Automation.Language.CommandAst]$Command, [Parameter(Mandatory)] [string]$ParameterName ) foreach ($element in $Command.CommandElements) { $isParameter = $element.GetType().Name -eq 'CommandParameterAst' if ($isParameter -and $ParameterName.StartsWith($element.ParameterName, 'OrdinalIgnoreCase')) { return $true } } $false } function Test-IslInsideFunction { <# .SYNOPSIS True when the node sits inside a function or a nested script block (so `return` or `exit` there doesn't necessarily end the script). #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Management.Automation.Language.Ast]$Node ) $parent = $Node.Parent while ($parent) { if ($parent.GetType().Name -eq 'FunctionDefinitionAst') { return $true } # A script block that isn't the script's own root body (e.g. a ForEach-Object block) if ($parent.GetType().Name -eq 'ScriptBlockExpressionAst') { return $true } $parent = $parent.Parent } $false } function Test-IslInsideTryWithCatch { <# .SYNOPSIS True when the node is inside the body of a try that has a catch block. #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Management.Automation.Language.Ast]$Node ) $child = $Node $parent = $Node.Parent while ($parent) { $isTry = $parent.GetType().Name -eq 'TryStatementAst' if ($isTry -and $parent.Body -eq $child -and $parent.CatchClauses.Count -gt 0) { return $true } $child = $parent $parent = $parent.Parent } $false } function Get-IslStringLiteral { <# .SYNOPSIS Every string literal in the script (constant and expandable), with its extent. #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Management.Automation.Language.Ast]$Ast ) Find-IslAstNode -Ast $Ast -TypeName StringConstantExpressionAst, ExpandableStringExpressionAst } |