Documentarian.DevX.Private.psm1
|
# Copyright (c) Microsoft Corporation. # Licensed under the MIT License. using namespace System.Management.Automation using namespace System.Management.Automation.Language #region Enums.Private enum ClassLogLevels { <# .SYNOPSIS Defines the logging levels for classes. .DESCRIPTION Defines the logging levels for classes. The logging level determines the amount of log messages that a class should write when it performs operations. .LABEL None The class shouldn't write any log messages. .LABEL Basic The class should write basic log messages. Equivalent to verbose logging. .LABEL Detailed The class should write detailed log messages. Equivalent to debug logging. #> None = 0 Basic = 1 Detailed = 2 } enum UncommonLineEndingAction { <# .SYNOPSIS Defines the action to take when an uncommon line ending is encountered. .DESCRIPTION Defines the action to take when an uncommon line ending is encountered. An uncommon line ending is a line ending that is not the standard line ending for the current platform. .LABEL SilentlyContinue Ignore the uncommon line ending and continue processing. .LABEL WarnAndContinue Emit a warning about the uncommon line ending and continue processing. .LABEL ErrorAndStop Emit an error about the uncommon line ending and stop processing. #> SilentlyContinue = 0 WarnAndContinue = 1 ErrorAndStop = 2 } #endregion Enums.Private #region Classes.Private class DevXAstTypeTransformAttribute : ArgumentTransformationAttribute { <# .SYNOPSIS Transforms input into a valid AST type. .DESCRIPTION This attribute transforms input into a valid AST type. The input can be a type object or a string representing the type name. The type must be in the `System.Management.Automation.Language` namespace and must end in `Ast`. If the input is not a valid AST type, an exception is thrown. #> #region Static properties #endregion Static properties #region Static methods #endregion Static methods #region Instance properties [object] Transform([EngineIntrinsics]$engineIntrinsics, [System.Object]$inputData) { $outputData = switch ($inputData) { { $_ -is [System.Type] } { if (Test-DevXIsAstType -Type $_) { $_ } else { $errorMessage = @( "Specified type '$($_.GetType().FullName)' is not an AST type;" "valid types must be in the 'System.Management.Automation.Language' namespace" "and must end in 'Ast'" 'You can use Get-AstType to discover valid AST Types' 'and to pass for this argument.' ) -join ' ' throw [ArgumentTransformationMetadataException]::New( $errorMessage ) } } { $_ -is [string] } { try { Get-AstType -Name $_ -ErrorAction Stop } catch { $errorMessage = @( "Specified type name '$_' is not an AST type;" "valid types must be in the 'System.Management.Automation.Language' namespace" "and must end in 'Ast'" 'You can use Get-AstType to discover valid AST Types' 'and to pass for this argument.' ) -join ' ' throw [ArgumentTransformationMetadataException]::New( $errorMessage ) } } default { $errorMessage = @( "Could not convert input ($_)" "of type '$($_.GetType().FullName)'" 'to a valid AST Type.' 'Specify the type itself or its name as a string.' 'You can use Get-AstType to discover valid AST Types' 'and to pass for this argument.' ) -join ' ' throw [ArgumentTransformationMetadataException]::New( $errorMessage ) } } return $outputData } #endregion Instance properties #region Instance methods #endregion Instance methods #region Constructors #endregion Constructors } class DevXValidatePowerShellScriptPathAttribute : ValidateArgumentsAttribute { <# .SYNOPSIS Validates that the specified path is for a valid PowerShell script file. .DESCRIPTION This attribute validates that the specified path is for a valid PowerShell script file. The path must be a file path to a PowerShell file with one of the following extensions: `.ps1`, `.psm1`, or `.psd1`. The file must exist and be accessible. #> #region Static properties #endregion Static properties #region Static methods #endregion Static methods #region Instance properties #endregion Instance properties #region Instance methods [void] Validate([object]$arguments, [EngineIntrinsics]$engineIntrinsics) { $path = $arguments if ([string]::IsNullOrWhiteSpace($path)) { throw [System.ArgumentNullException]::new() } try { $item = Get-Item -Path $path -ErrorAction Stop } catch [ItemNotFoundException] { throw [System.IO.FileNotFoundException]::new() } $messagePrefix = 'Path must be the file path to a PowerShell file' $provider = $Item.PSProvider.Name if ($provider -ne 'FileSystem') { throw [System.ArgumentException]::new( "$messagePrefix; specified provider '$provider' is invalid." ) } $extension = $Item.Extension $validExtensions = @('.psd1', '.ps1', '.psm1') $isNotPowerShellFile = $Extension -notin $ValidExtensions if ($isNotPowerShellFile) { $errorMessage = @( "Specified file '$path' has extension '$extension'," 'but it must be one of the following:' ($validExtensions -join ', ') ) -join ' ' throw [System.ArgumentException]::new("$messagePrefix; $errorMessage") } } #endregion Instance methods #region Constructors #endregion Constructors } class DevXAstInfo { <# .SYNOPSIS Class synopsis #> #region Static properties #endregion Static properties #region Static methods #endregion Static methods #region Instance properties <# .Synopsis The abstract syntax tree (AST) of the script block. .Description This property contains the abstract syntax tree (AST) of the script block. The AST represents the structure of the script block, including the statements, expressions, and other elements that make up the script block. The AST is generated by the PowerShell parser when the script block is parsed. #> [ScriptBlockAst] $Ast <# .Synopsis The tokens found when parsing the script block. .Description This property contains the tokens found when parsing the script block. The tokens include the comments from the script block, which aren't available in the AST. #> [Token[]] $Tokens <# .Synopsis The parse errors found when parsing the script block. .Description This property contains the parse errors found when parsing the script block. This property is only populated if there are errors in the script block that prevent it from being parsed. The presence of these errors indicates that the script block is not valid PowerShell code and needs attention. #> [ParseError[]] $Errors #endregion Instance properties #region Instance methods #endregion Instance methods #region Constructors DevXAstInfo([ScriptBlock]$scriptBlock) { <# .SYNOPSIS Initializes a new instance of DevXAstInfo from a script block. .DESCRIPTION This constructor creates a new instance of DevXAstInfo from a script block. It parses the script block into an AST and stores the AST, tokens, and errors in the new instance. .PARAMETER scriptBlock #> $t, $e = $null $this.Ast = [Parser]::ParseInput( $scriptBlock.ToString(), [ref]$t, [ref]$e ) $this.Tokens = $t $this.Errors = $e } DevXAstInfo([string]$path) { <# .SYNOPSIS Initializes a new instance of DevXAstInfo from a file path. .DESCRIPTION This constructor creates a new instance of DevXAstInfo from a file path. It parses the file into an AST and stores the AST, tokens, and errors in the new instance. .PARAMETER path The path to a PowerShell code file to parse into an AST. #> $t, $e = $null $this.Ast = [Parser]::ParseFile( $path, [ref]$t, [ref]$e ) $this.Tokens = $t $this.Errors = $e } DevXAstInfo([ScriptBlockAst]$ast) { <# .SYNOPSIS Initializes a new instance of DevXAstInfo from a script block AST. .DESCRIPTION This constructor creates a new instance of DevXAstInfo from a script block AST. It stores the AST in the new instance. This contructor can't define the tokens or errors as it doesn't parse the code itself. .PARAMETER ast The script block AST to store in the new instance. #> $this.Ast = $ast } DevXAstInfo([ScriptBlockAst]$ast, [Token[]]$tokens, [ParseError[]]$errors) { <# .SYNOPSIS Initializes a new instance of DevXAstInfo from an AST, tokens, and errors. .DESCRIPTION This constructor creates a new instance of DevXAstInfo from an AST, tokens, and errors. It stores the AST, tokens, and errors in the new instance. .PARAMETER ast The script block AST to store in the new instance. .PARAMETER tokens The tokens to store in the new instance. .PARAMETER errors The parse errors to store in the new instance. #> $this.Ast = $ast $this.Tokens = $tokens $this.Errors = $errors } #endregion Constructors } #endregion Classes.Private #region Functions.Private Function Find-Ast { <# .SYNOPSIS Search an AST for nodes that match a predicate. .DESCRIPTION This function searches an AST for nodes that match a predicate. The predicate can be a script block or a type. If a type is specified, the predicate is created from the type, returning any matching AST objects. You can specify the AST to search by providing an `AstInfo` object, a file path, or a script block. The function can search a single AST node or recurse through all child nodes. The function returns all matching nodes. .PARAMETER DevXAstInfo The `AstInfo` object to search. This parameter is mandatory if the **Path** or **ScriptBlock** parameters are not specified. .PARAMETER Path The file path to the script file containing the AST to search. This parameter is mandatory if the **DevXAstInfo** or **ScriptBlock** parameters are not specified. .PARAMETER ScriptBlock The script block containing the AST to search. This parameter is mandatory if the **DevXAstInfo** or **Path** parameters are not specified. .PARAMETER Predicate The predicates to use when searching the AST. This parameter is mandatory if the **Type** parameter is not specified. Each predicate must be a script block that returns `$true` if the AST node should be returned and otherwise `$false`. .PARAMETER Type The AST types to search for. This parameter is mandatory if the **Predicate** parameter is not specified. Each type must be a type object or a string representing the type name. The type must be in the `System.Management.Automation.Language` namespace and must end in `Ast`. .PARAMETER Recurse Indicates that the function should search all child nodes of the AST. By default, the function only searches the top-level nodes. .OUTPUTS System.Management.Automation.Language.Ast The function returns every AST node that matches the predicates or is in the type list. #> [CmdletBinding(DefaultParameterSetName = 'FromtAstInfo')] [OutputType([System.Management.Automation.Language.Ast])] Param( [Parameter(Mandatory, ParameterSetName = 'FromAstInfoWithPredicate')] [Parameter(Mandatory, ParameterSetName = 'FromAstInfoWithType')] [DevXAstInfo] $DevXAstInfo, [Parameter(Mandatory, ParameterSetName = 'FromPathWithPredicate')] [Parameter(Mandatory, ParameterSetName = 'FromPathWithType')] [string] $Path, [Parameter(Mandatory, ParameterSetName = 'FromScriptBlockWithPredicate')] [Parameter(Mandatory, ParameterSetName = 'FromScriptBlockWithType')] [scriptblock] $ScriptBlock, [Parameter(Mandatory, ParameterSetName = 'FromAstInfoWithPredicate')] [Parameter(Mandatory, ParameterSetName = 'FromPathWithPredicate')] [Parameter(Mandatory, ParameterSetName = 'FromScriptBlockWithPredicate')] [ScriptBlock[]] $Predicate, [Parameter(Mandatory, ParameterSetName = 'FromAstInfoWithType')] [Parameter(Mandatory, ParameterSetName = 'FromPathWithType')] [Parameter(Mandatory, ParameterSetName = 'FromScriptBlockWithType')] [DevXAstTypeTransformAttribute()] [System.Type[]] $Type, [Parameter()] [switch] $Recurse ) begin { } process { if (-not [string]::IsNullOrEmpty($Path)) { $DevXAstInfo = Get-Ast -Path $Path } elseif ($null -ne $ScriptBlock) { $DevXAstInfo = Get-Ast -ScriptBlock $ScriptBlock } if ($null -ne $Type) { $Predicate = New-AstPredicate -Type $Type } $Predicate | ForEach-Object -Process { $DevXAstInfo.Ast.FindAll($_, $Recurse) } } end { } } Function Get-Ast { <# .SYNOPSIS Gets the abstract syntax tree (AST) of a PowerShell script. .DESCRIPTION This function gets the abstract syntax tree (AST) of a PowerShell script. The AST represents the structure of the script, including the statements, expressions, and other elements that make up the script. The AST is generated by the PowerShell parser when the script is parsed. You can specify the script to parse by providing a file path, a script block, or a string containing the script text. .PARAMETER Path The file path to the script file to parse. The file must be a valid PowerShell code file that exists and is accessible. This parameter is mandatory if the **ScriptBlock** or **Text** parameters aren't specified. .PARAMETER ScriptBlock The script block to parse. This parameter is mandatory if the **Path** or **Text** parameters aren't specified. .PARAMETER Text The text of the script to parse. This parameter is mandatory if the **Path** or **ScriptBlock** parameters aren't specified. .OUTPUTs Returns an instance of the `DevXAstInfo` class that contains the AST, tokens, and errors found when parsing the script. #> [CmdletBinding()] [OutputType([DevXAstInfo])] param( [Parameter(Mandatory, ParameterSetName = 'ByPath')] [DevXValidatePowerShellScriptPathAttribute()] [string] $Path, [Parameter(Mandatory, ParameterSetName = 'ByScriptBlock')] [scriptblock] $ScriptBlock, [Parameter(Mandatory, ParameterSetName = 'ByInputText')] [ValidateNotNullOrEmpty()] [string] $Text ) begin { } process { switch ($PSCmdlet.ParameterSetName) { 'ByPath' { $Path = Resolve-Path -Path $Path -ErrorAction Stop [DevXAstInfo]::New($Path) } 'ByScriptBlock' { [DevXAstInfo]::New($ScriptBlock) } 'ByInputText' { [DevXAstInfo]::New([ScriptBlock]::Create($Text)) } } } end { } } Function Get-AstType { <# .SYNOPSIS Function synopsis. #> [CmdletBinding(DefaultParameterSetName = 'ByPattern')] [OutputType([Type])] Param( [Parameter( Mandatory, ParameterSetName = 'ByPattern', HelpMessage = 'Specify a valid regex pattern to match in the list of AST types' )] [string] $Pattern, [Parameter( Mandatory, ParameterSetName = 'ByName', HelpMessage = 'Specify a name to look for in the list of AST types; the "Ast" suffix is optional' )] [string[]] $Name ) begin { # ignore exceptions getting the types in an assembly $types = [System.AppDomain]::CurrentDomain.GetAssemblies() | ForEach-Object -Process { try { $_.GetTypes() } catch {} } | Where-Object -FilterScript { Test-DevXIsAstType -Type $_ } } process { if (-not [string]::IsNullOrEmpty($Pattern)) { $types | Where-Object -FilterScript { $_.Name -match $Pattern } } elseif ($Name.Count -gt 0) { foreach ($n in $Name) { $types | Where-Object -FilterScript { $_.Name -in @($n, "${n}Ast") } } } else { $types } } end { } } function Get-EnumRegex { <# .SYNOPSIS Function synopsis. #> [CmdletBinding()] [OutputType([String])] param( [Parameter(Mandatory)] [ValidateScript({ $_.IsEnum })] [Type] $EnumType ) begin { } process { "(?<$($EnumType.Name)>($($EnumType.GetEnumNames() -join '|')))" } end { } } Function New-AstPredicate { <# .SYNOPSIS Function synopsis. #> [CmdletBinding()] [OutputType([ScriptBlock])] Param ( [Parameter()] [DevXAstTypeTransformAttribute()] [System.Type[]] $Type ) begin { } process { foreach ($item in $Type) { { param([System.Management.Automation.Language.Ast]$AstObject) return ($AstObject -is $item) }.GetNewClosure() } } end { } } function New-SourceFile { <# .SYNOPSIS Function synopsis. #> [CmdletBinding()] [OutputType([SourceFile])] param( [Parameter()] [string] $NameSpace, [Parameter()] [string] $Path ) begin { } process { [SourceFile]::new($NameSpace, $Path) } end { } } function New-SourceFolder { <# .SYNOPSIS Function synopsis. #> [CmdletBinding()] [OutputType([SourceFolder])] param( [Parameter()] [string] $NameSpace, [Parameter(Mandatory)] [string] $Path ) begin { } process { if ($NameSpace) { [SourceFolder]::new($NameSpace, $Path) } else { [SourceFolder]::new($Path) } } end { } } function New-SourceReference { <# .SYNOPSIS Function synopsis. #> [CmdletBinding()] [OutputType([SourceReference])] param( [Parameter()] [SourceFile] $SourceFile, [Parameter()] [SourceFile[]] $Reference ) begin { } process { if ($Reference) { [SourceReference]::new($SourceFile, $Reference) } else { [SourceReference]::new($SourceFile) } } end { } } function Resolve-NameSpace { <# .SYNOPSIS Function synopsis #> [CmdletBinding()] [OutputType([String])] param( [Parameter(Mandatory, ParameterSetName = 'ByTaxonomy')] [SourceCategory] $Category, [Parameter(Mandatory, ParameterSetName = 'ByTaxonomy')] [SourceScope] $Scope, [Parameter(ParameterSetName = 'ByPath')] [string] $ParentNameSpace, [Parameter(Mandatory, ParameterSetName = 'ByPath')] [string] $Path ) begin { $baseFolderPattern = @( Get-EnumRegex -EnumType ([SourceScope]) '\w*\.' Get-EnumRegex -EnumType ([SourceCategory]) '\w*' '(?<Remaining>.+)?' ) -join '' } process { $ParentNameSpace = '' $childNameSpace = '' # If a path is specified, we need to figure out child namespacing if ($Path) { <# Turn the path containing the file into a period-separated string to avoid future path separator munging/checking for xplat concerns. #> $definitionFolder = Get-Item -Path $Path | Select-Object -ExpandProperty PSIsContainer | ForEach-Object { ($_ ? $Path : (Split-Path -Parent -Path $Path)) -replace '(\\|\/)', '.' } <# When there's no parent namespace, we need to figure out the category and scope to build the parent namespace and collect any remaining folder segments as child namespace segments. #> if ([string]::IsNullOrEmpty($ParentNameSpace)) { $sourceRelativeFolder = ($definitionFolder -split 'Source')[-1].Trim('.') if ($sourceRelativeFolder -match $baseFolderPattern) { $Category = [SourceCategory].GetEnumNames() | Where-Object { $Matches.SourceCategory -like "$_*" } $Scope = [SourceScope].GetEnumNames() | Where-Object { $Matches.SourceScope -like "$_*" } $childNameSpace = $Matches.Remaining } } else { <# When we know the parent, we just want everything after it. When parent is the base (only has two segments, Category.Scope) we want to split on Category. Otherwise, split on the last segment. #> $parentSegments = ($ParentNameSpace -split '\.') $splittingSegment = $parentSegments.Count -eq 2 ? $parentSegments[0] : $parentSegments[-1] $childNameSpace = $definitionFolder -split $splittingSegment | Select-Object -Skip 1 | Join-String -Separator '.' } } if ([string]::IsNullOrEmpty($ParentNameSpace)) { $ParentNameSpace = switch ($Category) { ArgumentCompleter { "ArgumentCompleters.$Scope" } Class { "Classes.$Scope" } Enum { "Enums.$Scope" } Function { "Functions.$Scope" } Format { "Formats.$Scope" } Task { "Tasks.$Scope" } Type { "Types.$Scope" } } } if ($Path) { return $ParentNameSpace + $ChildNameSpace } else { return $ParentNameSpace } } end { } } Function Test-DevXIsAstType { <# .SYNOPSIS Determines if a type is an AST type. #> [CmdletBinding()] [OutputType([bool])] Param( [Parameter()] [System.Type] $Type ) begin { } process { $inNamespace = $Type.NameSpace -eq 'System.Management.Automation.Language' $endsInAst = $Type.Name -match 'Ast$' return ($inNamespace -and $endsInAst) } end { } } #endregion Functions.Private |