Refactor.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\Refactor.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName Refactor.Import.DoDotSource -Fallback $false if ($Refactor_dotsourcemodule) { $script:doDotSource = $true } <# Note on Resolve-Path: All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. This is important when testing for paths. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = Get-PSFConfigValue -FullName Refactor.Import.IndividualFiles -Fallback $false if ($Refactor_importIndividualFiles) { $importIndividualFiles = $true } if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] Param ( [string] $Path ) $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath if ($doDotSource) { . $resolvedPath } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) { . Import-ModuleFile -Path $path } # Import all internal functions foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) { . Import-ModuleFile -Path $path } # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code <# This file loads the strings documents from the respective language folders. This allows localizing messages and errors. Load psd1 language files for each language you wish to support. Partial translations are acceptable - when missing a current language message, it will fallback to English or another available language. #> Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'Refactor' -Language 'en-US' function Get-AstCommand { <# .SYNOPSIS Parses out all commands contained in an AST. .DESCRIPTION Parses out all commands contained in an Abstract Syntax Tree. Will also resolve all parameters used as able and indicate, whether all could be identified. .PARAMETER Ast The Ast object to scan. .PARAMETER Splat Splat Data to use for parameter mapping .EXAMPLE PS C:\> Get-AstCommand -Ast $parsed.Ast -Splat $splats Returns all commands in the specified AST, mapping to the splats contained in $splats #> [OutputType([Refactor.CommandToken])] [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [System.Management.Automation.Language.Ast] $Ast, [AllowNull()] $Splat ) process { $splatHash = @{ } foreach ($splatItem in $Splat) { $splatHash[$splatItem.Ast] = $splatItem } $allCommands = Search-ReAst -Ast $Ast -Filter { $args[0] -is [System.Management.Automation.Language.CommandAst] } foreach ($command in $allCommands) { $result = [Refactor.CommandToken]::new($command.Data) # Splats foreach ($splatted in $command.Data.CommandElements | Where-Object Splatted) { $result.HasSplat = $true $splatItem = $splatHash[$splatted] if (-not $splatItem.ParametersKnown) { $result.ParametersKnown = $false } foreach ($parameterName in $splatItem.Parameters.Keys) { $result.parameters[$parameterName] = $parameterName } $result.Splats[$splatted] = $splatItem } $result } } } function Clear-ReTokenTransformationSet { <# .SYNOPSIS Remove all registered transformation sets. .DESCRIPTION Remove all registered transformation sets. .EXAMPLE PS C:\> Clear-ReTokenTransformationSet Removes all registered transformation sets. #> [CmdletBinding()] Param ( ) process { $script:tokenTransformations = @{ } } } function Convert-ReScriptFile { <# .SYNOPSIS Perform AST-based replacement / refactoring of scriptfiles .DESCRIPTION Perform AST-based replacement / refactoring of scriptfiles This process depends on two factors: + Token Provider + Token Transformation Sets The provider is a plugin that performs the actual AST analysis and replacement. For example, by default the "Command" provider allows renaming commands or their parameters. Use Register-ReTokenprovider to define your own plugin. Transformation Sets are rules that are applied to the tokens of a specific provider. For example, the "Command" provider could receive a rule that renames the command "Get-AzureADUser" to "Get-MgUser" Use Import-ReTokenTransformationSet to provide such rules. .PARAMETER Path Path to the scriptfile to modify. .PARAMETER Backup Whether to create a backup of the file before modifying it. .PARAMETER Force Whether to update files that end in ".backup.ps1" By default these are skipped, as they would be the backup-files of previous conversions ... or even the current one, when providing input via pipeline! .EXAMPLE PS C:\> Get-ChildItem C:\scripts -Recurse -Filter *.ps1 | Convert-ReScriptFile Converts all scripts under C:\scripts according to the provided transformation sets. #> [OutputType([Refactor.TransformationResult])] [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')] [Alias('FullName')] [string[]] $Path, [switch] $Backup, [switch] $Force ) process { foreach ($file in $Path | Resolve-PSFPath) { if (-not $Force -and $file -match '\.backup\.ps1$|\.backup\.psm1$') { continue } Write-PSFMessage -Message 'Processing file: {0}' -StringValues $file $scriptfile = [Refactor.ScriptFile]::new($file) try { $result = $scriptfile.Transform($scriptfile.GetTokens()) $scriptfile.Save($Backup.ToBool()) $result Write-PSFMessage -Message 'Finished processing file: {0} | Transform Count {1} | Success {2}' -StringValues $file, $result.Count, $result.Success } catch { Write-PSFMessage -Level Error -Message 'Failed to convert file: {0}' -StringValues $file -Target $scriptfile -ErrorRecord $_ -EnableException $true -PSCmdlet $PSCmdlet } } } } function Convert-ReScriptToken { <# .SYNOPSIS Converts a token using the conversion logic defined per token type. .DESCRIPTION Converts a token using the conversion logic defined per token type. This could mean renaming a command, changing a parameter, etc. The actual logic happens in the converter scriptblock provided by the Token Provider. This should update the changes in the Token object, as well as returning a summary object as output. .PARAMETER Token The token to transform. .PARAMETER Preview Instead of returning the new text for the token, return a metadata object providing additional information. .EXAMPLE PS C:\> Convert-ReScriptToken -Token $token Returns an object, showing what would have been done, had this been applied. #> [OutputType([Refactor.Change])] [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [Refactor.ScriptToken[]] $Token ) process { foreach ($tokenObject in $Token) { $provider = Get-ReTokenProvider -Name $tokenObject.Type if (-not $provider) { Stop-PSFFunction -Message "No provider found for type $($tokenObject.Type)" -Target $tokenObject -EnableException $true -Cmdlet $PSCmdlet } & $provider.Converter $tokenObject } } } function Get-ReScriptFile { <# .SYNOPSIS Reads a scriptfile and returns an object representing it. .DESCRIPTION Reads a scriptfile and returns an object representing it. Use this for custom transformation needs - for example to only process some select token kinds. .PARAMETER Path Path to the scriptfile to read. .EXAMPLE PS C:\> Get-ReScriptFile -Path C:\scripts\script.ps1 Reads in the specified scriptfile #> [OutputType([Refactor.ScriptFile])] [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')] [Alias('FullName')] [string[]] $Path ) process { foreach ($file in $Path | Resolve-PSFPath) { [Refactor.ScriptFile]::new($file) } } } function Get-ReSplat { <# .SYNOPSIS Resolves all splats in the offered Ast. .DESCRIPTION Resolves all splats in the offered Ast. This will look up any hashtable definitions and property-assignments to that hashtable, whether through property notation, index assignment or add method. It will then attempt to define an authorative list of properties assigned to that hashtable. If the result is unclear, that will be indicated accordingly. Return Objects include properties: + Splat : The original Ast where the hashtable is used for splatting + Parameters : A hashtable containing all properties clearly identified + ParametersKnown : Whether we are confident of having identified all properties passed through as parameters .PARAMETER Ast The Ast object to search. Use "Read-ReAst" to parse a scriptfile into an AST object. .EXAMPLE PS C:\> Get-ReSplat -Ast $ast Returns all splats used in the Abstract Syntax Tree object specified #> [OutputType([Refactor.Splat])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Management.Automation.Language.Ast] $Ast ) $splats = Search-ReAst -Ast $Ast -Filter { if ($args[0] -isnot [System.Management.Automation.Language.VariableExpressionAst]) { return $false } $args[0].Splatted } if (-not $splats) { return } foreach ($splat in $splats) { # Select the last variable declaration _before_ the splat is being used $assignments = Search-ReAst -Ast $Ast -Filter { if ($args[0] -isnot [System.Management.Automation.Language.AssignmentStatementAst]) { return $false } if ($args[0].Left -isnot [System.Management.Automation.Language.VariableExpressionAst]) { return $false } $args[0].Left.VariablePath.UserPath -eq $splat.Data.VariablePath.UserPath } $declaration = $assignments | Where-Object { $_.Start -lt $splat.Start } | Sort-Object { $_.Start } -Descending | Select-Object -First 1 $result = [Refactor.Splat]@{ Ast = $splat.Data } if (-not $declaration) { $result.ParametersKnown = $false $result continue } $propertyAssignments = Search-ReAst -Ast $Ast -Filter { if ($args[0].Extent.StartLineNumber -le $declaration.Start) { return $false } if ($args[0].Extent.StartLineNumber -ge $splat.Start) { return $false } $isAssignment = $( ($args[0] -is [System.Management.Automation.Language.AssignmentStatementAst]) -and ( ($args[0].Left.VariablePath.UserPath -eq $splat.Data.VariablePath.UserPath) -or ($args[0].Left.Expression.VariablePath.UserPath -eq $splat.Data.VariablePath.UserPath) -or ($args[0].Left.Target.VariablePath.UserPath -eq $splat.Data.VariablePath.UserPath) ) ) $isAddition = $( ($args[0] -is [System.Management.Automation.Language.InvokeMemberExpressionAst]) -and ($args[0].Expression.VariablePath.UserPath -eq $splat.Data.VariablePath.UserPath) -and ($args[0].Member.Value -eq 'Add') ) $isAddition -or $isAssignment } if ($declaration.Data.Right.Expression -isnot [System.Management.Automation.Language.HashtableAst]) { $result.ParametersKnown = $false } foreach ($pair in $declaration.Data.Right.Expression.KeyValuePairs) { if ($pair.Item1 -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $result.Parameters[$pair.Item1.Value] = $pair.Item1.Value } else { $result.ParametersKnown = $false } } foreach ($assignment in $propertyAssignments) { switch ($assignment.Type) { 'AssignmentStatementAst' { if ($assignment.Data.Left.Member -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $result.Parameters[$assignment.Data.Left.Member.Value] = $assignment.Data.Left.Member.Value continue } if ($assignment.Data.Left.Index -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $result.Parameters[$assignment.Data.Left.Index.Value] = $assignment.Data.Left.Index.Value continue } $result.ParametersKnown = $false } 'InvokeMemberExpressionAst' { if ($assignment.Data.Arguments[0] -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $result.Parameters[$assignment.Data.Arguments[0].Value] = $assignment.Data.Arguments[0].Value continue } $result.ParametersKnown = $false } } } # Include all relevant Ast objects $result.Assignments = @($declaration.Data) + @($propertyAssignments.Data) | Remove-PSFNull -Enumerate $result } } function Get-ReTokenProvider { <# .SYNOPSIS List registered token providers. .DESCRIPTION List registered token providers. Token providers are scriptblocks that will parse an Abstract Syntax Tree, searching for specific types of code content. These can then be used for code analysis or refactoring. .PARAMETER Name Name of the provider to filter by. Defaults to "*" .PARAMETER Component Return only the specified component: + All: Return the entire provider + Tokenizer: Return only the scriptblock, that parses out the Ast + Converter: Return only the scriptblock, that applies transforms to tokens Default: All .EXAMPLE PS C:\> Get-ReTokenProvider List all token providers #> [OutputType([Refactor.TokenProvider])] [CmdletBinding()] Param ( [string] $Name = '*', [ValidateSet('All','Tokenizer','Converter')] [string] $Component = 'All' ) process { foreach ($provider in $script:tokenProviders.GetEnumerator()) { if ($provider.Key -notlike $Name) { continue } if ($Component -eq 'Tokenizer') { $provider.Value.Tokenizer continue } if ($Component -eq 'Converter') { $provider.Value.Converter continue } $provider.Value } } } function Get-ReTokenTransformationSet { <# .SYNOPSIS List the registered transformation sets. .DESCRIPTION List the registered transformation sets. .PARAMETER Type The type of token to filter by. Defaults to '*' .EXAMPLE PS C:\> Get-ReTokenTransformationSet Return all registerd transformation sets. #> [CmdletBinding()] param ( [string] $Type = '*' ) process { foreach ($pair in $script:tokenTransformations.GetEnumerator()) { if ($pair.Key -notlike $Type) { continue } $pair.Value.Values } } } function Import-ReTokenTransformationSet { <# .SYNOPSIS Imports a token transformation file. .DESCRIPTION Imports a token transformation file. Can be either json or psd1 format Root level must contain at least three nodes: + Version: The schema version of this file. Should be 1 + Type: The type of token being transformed. E.g.: "Command" + Content: A hashtable containing the actual sets of transformation. The properties required depend on the Token Provider. Example: @{ Version = 1 Type = 'Command' Content = @{ "Get-AzureADUser" = @{ Name = "Get-AzureADUser" NewName = "Get-MgUser" Comment = "Filter and search parameters cannot be mapped straight, may require manual attention" Parameters = @{ Search = "Filter" # Rename Search on "Get-AzureADUser" to "Filter" on "Get-MgUser" } } } } .PARAMETER Path Path to the file to import. Must be json or psd1 format .EXAMPLE PS C:\> Import-ReTokenTransformationSet -Path .\azureAD-to-graph.psd1 Imports all the transformationsets stored in "azureAD-to-graph.psd1" in the current folder. #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [string[]] $Path ) begin { function Import-TransformV1 { [CmdletBinding()] param ( $Data, $Path ) $msgDefault = @{ Level = "Warning" FunctionName = 'Import-ReTokenTransformationSet' PSCmdlet = $PSCmdlet StringValues = $Path } $defaultType = $Data.Type foreach ($entry in $Data.Content.Values) { $entryHash = $entry | ConvertTo-PSFHashtable if ($defaultType -and -not $entryHash.Type) { $entryHash.Type = $defaultType } if (-not $entryHash.Type) { Write-PSFMessage @msgDefault -Message "Invalid entry within file - No Type defined: {0}" -Target $entryHash continue } try { Register-ReTokenTransformation @entryHash -ErrorAction Stop } catch { Write-PSFMessage @msgDefault -Message "Error processing entry within file: {0}" -ErrorRecord $_ -Target $entryHash continue } } } } process { :main foreach ($filePath in $Path | Resolve-PSFPath -Provider FileSystem) { if (Test-Path -LiteralPath $filePath -PathType Container) { continue } $fileInfo = Get-Item -LiteralPath $filePath $data = switch ($fileInfo.Extension) { '.json' { Get-Content -LiteralPath $fileInfo.FullName | ConvertFrom-Json } '.psd1' { Import-PSFPowerShellDataFile -LiteralPath $fileInfo.FullName } default { $exception = [System.ArgumentException]::new("Unknown file extension: $($fileInfo.Extension)") Write-PSFMessage -Message "Error importing $($fileInfo.FullName): Unknown file extension: $($fileInfo.Extension)" -Level Error -Exception $exception -EnableException $true -Target $fileInfo -OverrideExceptionMessage continue main } } switch ("$($data.Version)") { "1" { Import-TransformV1 -Data $data -Path $fileInfo.FullName } default { $exception = [System.ArgumentException]::new("Unknown schema version: $($data.Version)") Write-PSFMessage -Message "Error importing $($fileInfo.FullName): Unknown schema version: $($data.Version)" -Level Error -Exception $exception -EnableException $true -Target $fileInfo -OverrideExceptionMessage continue main } } } } } function New-ReToken { <# .SYNOPSIS Creates a new, generic token object. .DESCRIPTION Creates a new, generic token object. Use this in script-only Token Providers, trading the flexibility of a custom Token type for the simplicity of not having to deal with C# or classes. .PARAMETER Type The type of the token. Must match the name of the provider using it. .PARAMETER Name The name of the token. Used to match the token against transforms. .PARAMETER Ast An Ast object representing the location in the script the token deals with. Purely optional, so long as your provider knows how to deal with the token. .PARAMETER Data Any additional data to store with the token. .EXAMPLE PS C:\> New-ReToken -Type variable -Name ComputerName Creates a new token of type variable with name ComputerName. Assumes you have registered a Token Provider of name variable. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [OutputType([Refactor.GenericToken])] [CmdletBinding()] param ( [parameter(Mandatory = $true)] [string] $Type, [parameter(Mandatory = $true)] [string] $Name, [System.Management.Automation.Language.Ast] $Ast, [object] $Data ) process { $token = [Refactor.GenericToken]::new($Type, $Name) $token.Ast = $Ast $token.Data = $Data $token } } function Read-ReAst { <# .SYNOPSIS Parse the content of a script .DESCRIPTION Uses the powershell parser to parse the content of a script or scriptfile. .PARAMETER ScriptCode The scriptblock to parse. .PARAMETER Path Path to the scriptfile to parse. Silently ignores folder objects. .EXAMPLE PS C:\> Read-Ast -ScriptCode $ScriptCode Parses the code in $ScriptCode .EXAMPLE PS C:\> Get-ChildItem | Read-ReAst Parses all script files in the current directory #> [CmdletBinding()] param ( [Parameter(Position = 0, ParameterSetName = 'Script', Mandatory = $true)] [System.Management.Automation.ScriptBlock] $ScriptCode, [Parameter(Mandatory = $true, ParameterSetName = 'File', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [string[]] $Path ) process { foreach ($file in $Path) { Write-PSFMessage -Level Verbose -Message "Processing $file" -Target $file $item = Get-Item $file if ($item.PSIsContainer) { Write-PSFMessage -Level Verbose -Message "is folder, skipping $file" -Target $file continue } $tokens = $null $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($item.FullName, [ref]$tokens, [ref]$errors) [pscustomobject]@{ Ast = $ast Tokens = $tokens Errors = $errors File = $item.FullName } } if ($ScriptCode) { $tokens = $null $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseInput($ScriptCode, [ref]$tokens, [ref]$errors) [pscustomobject]@{ Ast = $ast Tokens = $tokens Errors = $errors Source = $ScriptCode } } } } function Read-ReScriptCommand { <# .SYNOPSIS Reads a scriptfile and returns all commands contained within. .DESCRIPTION Reads a scriptfile and returns all commands contained within. Includes parameters used and whether all parameters could be resolved. .PARAMETER Path Path to the file to scan .PARAMETER Ast An already provided Abstract Syntax Tree object to process .EXAMPLE Get-ChildItem C:\scripts -Recurse -Filter *.ps1 | Read-ReScriptCommand Returns all commands in all files under C:\scripts #> [OutputType([Refactor.CommandToken])] [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Path')] [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')] [Alias('FullName')] [string[]] $Path, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Ast')] [System.Management.Automation.Language.Ast] $Ast ) process { if ($Path) { foreach ($file in $Path | Resolve-PSFPath) { $parsed = Read-ReAst -Path $file $splats = Get-ReSplat -Ast $parsed.Ast Get-AstCommand -Ast $parsed.Ast -Splat $splats } } foreach ($astObject in $Ast) { $splats = Get-ReSplat -Ast $astObject Get-AstCommand -Ast $astObject -Splat $splats } } } function Register-ReTokenProvider { <# .SYNOPSIS Register a Token Provider, that implements scanning and refactor logic. .DESCRIPTION Register a Token Provider, that implements scanning and refactor logic. For example, the "Command" Token Provider supports: - Finding all commands called in a script, resolving all parameters used as possible. - Renaming commands and their parameters. For examples on how to implement this, see: Provider: https://github.com/FriedrichWeinmann/Refactor/blob/development/Refactor/internal/tokenProvider/command.token.ps1 Token Class: https://github.com/FriedrichWeinmann/Refactor/blob/development/library/Refactor/Refactor/CommandToken.cs Note: Rather than implementing your on Token Class, you can use New-ReToken and the GenericToken class. This allows you to avoid the need for coding your own class, but offers no extra functionality. .PARAMETER Name Name of the token provider. .PARAMETER TransformIndex The property name used to map a transformation rule to a token. .PARAMETER ParametersMandatory The parameters a transformation rule MUST have to be valid. .PARAMETER Parameters The parameters a transformation rule accepts / supports. .PARAMETER Tokenizer Code that provides the required tokens when executed. Accepts one argument: An Ast object. .PARAMETER Converter Code that applies the registered transformation rule to a given token. Accepts two arguments: A Token and a boolean. The boolean argument representing, whether a preview object, representing the expected changes should be returned. .EXAMPLE PS C:\> Register-ReTokenProvider @param Registers a token provider. A useful example for what to provide is a bit more than can be fit in an example block, See an example provider here: Provider: https://github.com/FriedrichWeinmann/Refactor/blob/development/Refactor/internal/tokenProvider/command.token.ps1 Token Class: https://github.com/FriedrichWeinmann/Refactor/blob/development/library/Refactor/Refactor/CommandToken.cs #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Name, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $TransformIndex, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $ParametersMandatory, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $Parameters, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ScriptBlock] $Tokenizer, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ScriptBlock] $Converter ) process { $script:tokenProviders[$Name] = [Refactor.TokenProvider]@{ Name = $Name TransformIndex = $TransformIndex TransformParametersMandatory = $ParametersMandatory TransformParameters = $Parameters Tokenizer = $Tokenizer Converter = $Converter } } } function Register-ReTokenTransformation { <# .SYNOPSIS Register a transformation rule used when refactoring scripts. .DESCRIPTION Register a transformation rule used when refactoring scripts. Rules are specific to their token type. Different types require different parameters, which are added via dynamic parameters. For more details, look up the documentation for the specific token type you want to register a transformation for. .PARAMETER Type The type of token to register a transformation over. .EXAMPLE PS C:\> Register-ReTokenTransformation -Type Command -Name Get-AzureADUser -NewName Get-MGUser -Comment "The filter parameter requires manual adjustments if used" Registers a transformation rule, that will convert the Get-AzureADUser command to Get-MGUser #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Type ) DynamicParam { $parameters = (Get-ReTokenProvider -Name $Type).TransformParameters if (-not $parameters) { return } $results = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary foreach ($parameter in $parameters) { $parameterAttribute = New-Object System.Management.Automation.ParameterAttribute $parameterAttribute.ParameterSetName = '__AllParameterSets' $attributesCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] $attributesCollection.Add($parameterAttribute) $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter($parameter, [object], $attributesCollection) $results.Add($parameter, $RuntimeParam) } $results } begin { $commonParam = 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable' } process { $provider = Get-ReTokenProvider -Name $Type if (-not $provider) { Stop-PSFFunction -Message "No provider found for type $Type" -Target $PSBoundParameters -EnableException $true -Cmdlet $PSCmdlet } $hash = $PSBoundParameters | ConvertTo-PSFHashtable -Exclude $commonParam $missingMandatory = $provider.TransformParametersMandatory | Where-Object { $_ -notin $hash.Keys } if ($missingMandatory) { Stop-PSFFunction -Message "Error defining a $($Type) transformation: $($provider.TransformParametersMandatory -join ",") must be specified! Missing: $($missingMandatory -join ",")" -Target $PSBoundParameters -EnableException $true -Cmdlet $PSCmdlet } if (-not $script:tokenTransformations[$Type]) { $script:tokenTransformations[$Type] = @{ } } $script:tokenTransformations[$Type][$hash.$($provider.TransformIndex)] = [PSCustomObject]$hash } } function Search-ReAst { <# .SYNOPSIS Tool to search the Abstract Syntax Tree .DESCRIPTION Tool to search the Abstract Syntax Tree .PARAMETER Ast The Ast to search .PARAMETER Filter The filter condition to apply .EXAMPLE PS C:\> Search-ReAst -Ast $ast -Filter { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } Searches for all function definitions #> [OutputType([Refactor.SearchResult])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Management.Automation.Language.Ast] $Ast, [Parameter(Mandatory = $true)] [ScriptBlock] $Filter ) process { $results = $Ast.FindAll($Filter, $true) foreach ($result in $results) { [Refactor.SearchResult]::new($result) } } } <# This is an example configuration file By default, it is enough to have a single one of them, however if you have enough configuration settings to justify having multiple copies of it, feel totally free to split them into multiple files. #> <# # Example Configuration Set-PSFConfig -Module 'Refactor' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" #> Set-PSFConfig -Module 'Refactor' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." Set-PSFConfig -Module 'Refactor' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." <# Stored scriptblocks are available in [PsfValidateScript()] attributes. This makes it easier to centrally provide the same scriptblock multiple times, without having to maintain it in separate locations. It also prevents lengthy validation scriptblocks from making your parameter block hard to read. Set-PSFScriptblock -Name 'Refactor.ScriptBlockName' -Scriptblock { } #> <# # Example: Register-PSFTeppScriptblock -Name "Refactor.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' } #> <# # Example: Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name Refactor.alcohol #> New-PSFLicense -Product 'Refactor' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2022-03-05") -Text @" Copyright (c) 2022 Friedrich Weinmann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "@ # Module-wide storage for token provider scriptblocks $script:tokenProviders = @{ } # Transformation rules for tokens $script:tokenTransformations = @{ } $tokenizer = { Read-ReScriptCommand -Ast $args[0] } $converter = { param ( [Refactor.ScriptToken] $Token ) $transform = Get-ReTokenTransformationSet -Type Command | Where-Object Name -EQ $Token.Name if ($transform.MsgInfo) { $Token.WriteMessage('Information', $transform.MsgInfo, $transform) } if ($transform.MsgWarning) { $Token.WriteMessage('Warning', $transform.MsgWarning, $transform) } if ($transform.MsgError) { $Token.WriteMessage('Error', $transform.MsgError, $transform) } $changed = $false $items = foreach ($commandElement in $Token.Ast.CommandElements) { # Command itself if ($commandElement -eq $Token.Ast.CommandElements[0]) { if ($transform.NewName) { $transform.NewName; $changed = $true } else { $commandElement.Value } continue } if ($commandElement -isnot [System.Management.Automation.Language.CommandParameterAst]) { $commandElement.Extent.Text continue } if (-not $transform.Parameters) { $commandElement.Extent.Text continue } # Not guaranteed to be a hashtable $transform.Parameters = $transform.Parameters | ConvertTo-PSFHashtable if (-not $transform.Parameters[$commandElement.ParameterName]) { $commandElement.Extent.Text continue } "-$($transform.Parameters[$commandElement.ParameterName])" $changed = $true } #region Conditional Messages if ($transform.InfoParameters) { $transform.InfoParameters | ConvertTo-PSFHashtable } foreach ($parameter in $transform.InfoParameters.Keys) { if ($Token.Parameters[$parameter]) { $Token.WriteMessage('Information', $transform.InfoParameters[$parameter], $transform) } } if ($transform.WarningParameters) { $transform.WarningParameters | ConvertTo-PSFHashtable } foreach ($parameter in $transform.WarningParameters.Keys) { if ($Token.Parameters[$parameter]) { $Token.WriteMessage('Warning', $transform.WarningParameters[$parameter], $transform) } } if ($transform.ErrorParameters) { $transform.ErrorParameters | ConvertTo-PSFHashtable } foreach ($parameter in $transform.ErrorParameters.Keys) { if ($Token.Parameters[$parameter]) { $Token.WriteMessage('Error', $transform.ErrorParameters[$parameter], $transform) } } if (-not $Token.ParametersKnown) { if ($transform.UnknownInfo) { $Token.WriteMessage('Information', $transform.UnknownInfo, $transform) } if ($transform.UnknownWarning) { $Token.WriteMessage('Warning', $transform.UnknownInfo, $transform) } if ($transform.UnknownError) { $Token.WriteMessage('Error', $transform.UnknownInfo, $transform) } } #endregion Conditional Messages $Token.NewText = $items -join " " if (-not $changed) { $Token.NewText = $Token.Text } #region Add changes for splat properties foreach ($property in $Token.Splats.Values.Parameters.Keys) { if ($transform.Parameters.Keys -notcontains $property) { continue } foreach ($ast in $Token.Splats.Values.Assignments) { #region Case: Method Invocation if ($ast -is [System.Management.Automation.Language.InvokeMemberExpressionAst]) { if ($ast.Arguments[0].Value -ne $property) { continue } $Token.AddChange($ast.Arguments[0].Extent.Text, ("'{0}'" -f ($transform.Parameters[$property] -replace "^'|'$|^`"|`"$")), $ast.Arguments[0].Extent.StartOffset, $ast) continue } #endregion Case: Method Invocation #region Case: Original assignment if ($ast.Left -is [System.Management.Automation.Language.VariableExpressionAst]) { foreach ($hashKey in $ast.Right.Expression.KeyValuePairs.Item1) { if ($hashKey.Value -ne $property) { continue } $Token.AddChange($hashKey.Extent.Text, ("'{0}'" -f ($transform.Parameters[$property] -replace "^'|'$|^`"|`"$")), $hashKey.Extent.StartOffset, $hashKey) } continue } #endregion Case: Original assignment #region Case: Property assignment if ($ast.Left -is [System.Management.Automation.Language.MemberExpressionAst]) { if ($ast.Left.Member.Value -ne $property) { continue } $Token.AddChange($ast.Left.Member.Extent.Text, $transform.Parameters[$property], $ast.Left.Member.Extent.StartOffset, $ast) continue } #endregion Case: Property assignment #region Case: Index assignment if ($ast.Left -is [System.Management.Automation.Language.IndexExpressionAst]) { if ($ast.Left.Index.Value -ne $property) { continue } $Token.AddChange($ast.Left.Index.Extent.Text, ("'{0}'" -f ($transform.Parameters[$property] -replace "^'|'$|^`"|`"$")), $ast.Left.Index.Extent.StartOffset, $ast) continue } #endregion Case: Index assignment } } #endregion Add changes for splat properties # Return changes $Token.GetChanges() } $parameters = @( 'Name' 'NewName' 'Parameters' 'MsgInfo' 'MsgWarning' 'MsgError' 'InfoParameters' 'WarningParameters' 'ErrorParameters' 'UnknownInfo' 'UnknownWarning' 'UnknownError' ) $param = @{ Name = 'Command' TransformIndex = 'Name' ParametersMandatory = 'Name' Parameters = $parameters Tokenizer = $tokenizer Converter = $converter } Register-ReTokenProvider @param #endregion Load compiled code |