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 Find-BreakingChange {
    <#
    .SYNOPSIS
        Search a given AST for any breaking change contained.
     
    .DESCRIPTION
        Search a given AST for any breaking change contained.
 
        Use Import-ReBreakingChange to load definitions of breaking changes to look for.
     
    .PARAMETER Ast
        The AST to search
     
    .PARAMETER Name
        The name of the file being searched.
        Use this to identify non-filesystem code.
     
    .PARAMETER Changes
        The breaking changes to look out for.
     
    .EXAMPLE
        PS C:\> Find-BreakingChange -Ast $ast -Changes $changes
 
        Find all instances of breaking changes found within $ast.
    #>

    [OutputType([Refactor.BreakingChange])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.Language.Ast]
        $Ast,

        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [hashtable]
        $Changes
    )

    if (-not $Name) { $Name = $Ast.Extent.File }
    $filePath = $Name
    $fileName = ($Name -split "\\|/")[-1]

    $commands = Read-ReScriptCommand -Ast $Ast
    foreach ($commandToken in $commands) {
        foreach ($change in $Changes[$commandToken.Name]) {
            if ($change.Parameters.Count -lt 1) {
                [Refactor.BreakingChange]@{
                    Path        = $filePath
                    Name        = $fileName
                    Line        = $commandToken.Line
                    Command     = $commandToken.Name
                    Type        = 'Error'
                    Description = $change.Description
                    Module      = $change.Module
                    Version     = $change.Version
                    Tags        = $change.Tags
                }
                continue
            }

            foreach ($parameter in $change.Parameters.Keys) {
                if ($commandToken.Parameters.Keys -contains $parameter) {
                    [Refactor.BreakingChange]@{
                        Path        = $filePath
                        Name        = $fileName
                        Line        = $commandToken.Line
                        Command     = $commandToken.Name
                        Parameter   = $parameter
                        Type        = 'Error'
                        Description = $change.Parameters.$parameter
                        Module      = $change.Module
                        Version     = $change.Version
                        Tags        = $change.Tags
                    }
                    continue
                }

                if ($commandToken.ParametersKnown) { continue }

                [Refactor.BreakingChange]@{
                    Path        = $filePath
                    Name        = $fileName
                    Line        = $commandToken.Line
                    Command     = $commandToken.Name
                    Parameter   = $parameter
                    Type        = 'Warning'
                    Description = "Not all parameters on command resolveable - might be in use. $($change.Parameters.$parameter)"
                    Module      = $change.Module
                    Version     = $change.Version
                    Tags        = $change.Tags
                }
            }
        }
    }
}

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 ProviderName
        Name of the Token Provider to apply.
        Defaults to: '*'
     
    .PARAMETER Backup
        Whether to create a backup of the file before modifying it.
 
    .PARAMETER OutPath
        Folder to which to write the converted scriptfile.
 
    .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!
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .EXAMPLE
        PS C:\> Get-ChildItem C:\scripts -Recurse -Filter *.ps1 | Convert-ReScriptFile
 
        Converts all scripts under C:\scripts according to the provided transformation sets.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [OutputType([Refactor.TransformationResult])]
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'inplace')]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')]
        [Alias('FullName')]
        [string[]]
        $Path,

        [PsfArgumentCompleter('Refactor.TokenProvider')]
        [string[]]
        $ProviderName = '*',

        [Parameter(ParameterSetName = 'inplace')]
        [switch]
        $Backup,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'path')]
        [PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')]
        [string]
        $OutPath,

        [switch]
        $Force
    )
    
    begin {
        $lastResolvedPath = ""
    }
    process
    {
        if ($OutPath -ne $lastResolvedPath) {
            $resolvedOutPath = Resolve-PSFPath -Path $OutPath
            $lastResolvedPath = $OutPath
        }
        foreach ($file in $Path | Resolve-PSFPath) {
            if (-not $Force -and -not $OutPath -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($ProviderName))
            }
            catch {
                Write-PSFMessage -Level Error -Message 'Failed to convert file: {0}' -StringValues $file -Target $scriptfile -ErrorRecord $_ -EnableException $true -PSCmdlet $PSCmdlet
            }

            if ($OutPath) {
                Invoke-PSFProtectedCommand -Action 'Replacing content of script' -Target $file -ScriptBlock {
                    $scriptfile.WriteTo($resolvedOutPath, "")
                } -PSCmdlet $PSCmdlet -EnableException $EnableException -Continue
            }
            else {
                Invoke-PSFProtectedCommand -Action 'Replacing content of script' -Target $file -ScriptBlock {
                    $scriptfile.Save($Backup.ToBool())
                } -PSCmdlet $PSCmdlet -EnableException $EnableException -Continue
            }
            $result
            Write-PSFMessage -Message 'Finished processing file: {0} | Transform Count {1} | Success {2}' -StringValues $file, $result.Count, $result.Success
        }
    }
}

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.
 
    .PARAMETER Name
        The name of the script.
        Used for identifying scriptcode that is not backed by an actual file.
 
    .PARAMETER Content
        The code of the script.
        Used to provide scriptcode without requiring the backing of a file.
     
    .EXAMPLE
        PS C:\> Get-ReScriptFile -Path C:\scripts\script.ps1
 
        Reads in the specified scriptfile
    #>

    [OutputType([Refactor.ScriptFile])]
    [CmdletBinding(DefaultParameterSetName = 'Path')]
    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, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Content')]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Content')]
        [AllowEmptyString()]
        [string]
        $Content
    )
    process {
        if ($Path) {
            foreach ($file in $Path | Resolve-PSFPath) {
                [Refactor.ScriptFile]::new($file)
            }
        }

        if ($Name -and $PSBoundParameters.ContainsKey('Content')) {
            [Refactor.ScriptFile]::new($Name, $Content)
        }
    }
}

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-ReToken {
    <#
    .SYNOPSIS
        Scans a scriptfile for all tokens contained within.
     
    .DESCRIPTION
        Scans a scriptfile for all tokens contained within.
     
    .PARAMETER Path
        Path to the file to scan
     
    .PARAMETER ProviderName
        Names of the providers to use.
        Defaults to '*'
     
    .EXAMPLE
        PS C:\> Get-ChildItem C:\scripts | Get-ReToken
 
        Returns all tokens for all scripts under C:\scripts
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')]
        [Alias('FullName')]
        [string[]]
        $Path,

        [PsfArgumentCompleter('Refactor.TokenProvider')]
        [string[]]
        $ProviderName = '*'
    )

    process {
        foreach ($file in $Path | Resolve-PSFPath) {
            Write-PSFMessage -Message 'Processing file: {0}' -StringValues $file
            $scriptfile = [Refactor.ScriptFile]::new($file)
            $scriptfile.GetTokens($ProviderName)
        }
    }
}

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 (
        [PsfArgumentCompleter('Refactor.TokenProvider')]
        [string[]]
        $Name = '*',

        [ValidateSet('All','Tokenizer','Converter')]
        [string]
        $Component = 'All'
    )
    
    process {
        foreach ($provider in $script:tokenProviders.GetEnumerator()) {
            $matched = $false
            foreach ($nameFilter in $Name) {
                if ($provider.Key -like $nameFilter) { $matched = $true }
            }
            if (-not $matched) { 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
            $contentHash = $Data.Content | ConvertTo-PSFHashtable
            foreach ($entry in $contentHash.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)]
        [string]
        $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)
        {
            if (-not $content) { $content = $ScriptCode }
            $tokens = $null
            $errors = $null
            $ast = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$tokens, [ref]$errors)
            [pscustomobject]@{
                Ast           = $ast
                Tokens       = $tokens
                Errors       = $errors
                Source       = $ScriptCode
            }
        }
    }
}

function Read-ReAstComponent {
    <#
    .SYNOPSIS
        Search for instances of a given AST type.
     
    .DESCRIPTION
        Search for instances of a given AST type.
        This command - together with its sibling command "Write-ReAstComponent" - is designed to simplify code updates.
 
        Use the data on the object, update its "NewText" property and use the "Write"-command to apply it back to the original document.
     
    .PARAMETER Name
        Name of the "file" to search.
        Use this together with the 'ScriptCode' parameter when you do not actually have a file object and just the code itself.
        Usually happens when scanning a git repository or otherwise getting the data from some API/service.
     
    .PARAMETER ScriptCode
        Code of the "file" to search.
        Use this together with the 'Name' parameter when you do not actually have a file object and just the code itself.
        Usually happens when scanning a git repository or otherwise getting the data from some API/service.
     
    .PARAMETER Path
        Path to the file to scan.
        Uses wildcards to interpret results.
     
    .PARAMETER LiteralPath
        Literal path to the file to scan.
        Does not interpret the path and instead use it as it is written.
        Useful when there are brackets in the filename.
     
    .PARAMETER Select
        The AST types to select for.
 
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Get-ChildItem -Recurse -Filter *.ps1 | Read-ReAstComponent -Select FunctionDefinitionAst, ForEachStatementAst
         
        Reads all ps1 files in the current folder and subfolders and scans for all function definitions and foreach statements.
    #>

    [OutputType([Refactor.Component.AstResult])]
    [CmdletBinding(DefaultParameterSetName = 'File')]
    param (
        [Parameter(Position = 0, ParameterSetName = 'Script', Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $Name,
        
        [Parameter(Position = 1, ParameterSetName = 'Script', Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Content')]
        [string]
        $ScriptCode,

        [Parameter(Mandatory = $true, ParameterSetName = 'File', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [string[]]
        $Path,

        [Parameter(Mandatory = $true, ParameterSetName = 'Literal')]
        [string[]]
        $LiteralPath,

        [Parameter(Mandatory = $true)]
        [PsfArgumentCompleter('Refactor.AstTypes')]
        [PsfValidateSet(TabCompletion = 'Refactor.AstTypes')]
        [string[]]
        $Select,

        [switch]
        $EnableException
    )

    process {
        #region Resolve Targets
        $targets = [System.Collections.ArrayList]@()
        if ($Name) {
            $null = $targets.Add(
                [PSCustomObject]@{
                    Name    = $Name
                    Content = $ScriptCode
                    Path    = ''
                }
            )
        }
        foreach ($pathEntry in $Path) {
            try { $resolvedPaths = Resolve-PSFPath -Path $pathEntry -Provider FileSystem }
            catch {
                Write-PSFMessage -Level Warning -Message 'Failed to resolve path: {0}' -StringValues $pathEntry -ErrorRecord $_ -EnableException $EnableException
                continue
            }

            foreach ($resolvedPath in $resolvedPaths) {
                $null = $targets.Add(
                    [PSCustomObject]@{
                        Name    = Split-Path -Path $resolvedPath -Leaf
                        Path    = $resolvedPath
                    }
                )
            }
        }
        foreach ($pathEntry in $LiteralPath) {
            try { $resolvedPath = (Get-Item -LiteralPath $pathEntry -ErrorAction Stop).FullName }
            catch {
                Write-PSFMessage -Level Warning -Message 'Failed to resolve path: {0}' -StringValues $pathEntry -ErrorRecord $_ -EnableException $EnableException
                continue
            }

            $null = $targets.Add(
                [PSCustomObject]@{
                    Name    = Split-Path -Path $resolvedPath -Leaf
                    Path    = $resolvedPath
                }
            )
        }
        #endregion Resolve Targets

        Clear-ReTokenTransformationSet
        Register-ReTokenTransformation -Type ast -TypeName $Select

        foreach ($target in $targets) {
            # Create ScriptFile object
            if ($target.Path) {
                $scriptFile = [Refactor.ScriptFile]::new($target.Path)
            }
            else {
                $scriptFile = [Refactor.ScriptFile]::new($target.Name, $target.Content)
            }

            # Generate Tokens
            $tokens = $scriptFile.GetTokens('Ast')

            # Profit!
            $result = [Refactor.Component.ScriptResult]::new()
            $result.File = $scriptFile
            $result.Types = $Select
            foreach ($token in $tokens) { $result.Tokens.Add($token) }

            foreach ($token in $tokens) {
                [Refactor.Component.AstResult]::new($token, $scriptFile, $result)
            }
        }
        
        Clear-ReTokenTransformationSet
    }
}

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)
        }
    }
}

function Test-ReSyntax {
    <#
    .SYNOPSIS
        Tests whether the syntax of a given scriptfile or scriptcode is valid.
     
    .DESCRIPTION
        Tests whether the syntax of a given scriptfile or scriptcode is valid.
        This uses the PowerShell syntax validation.
        Some cases - especially around PowerShell classes - may evaluate as syntax error when missing dependencies.
     
    .PARAMETER Path
        Path to the file to test.
     
    .PARAMETER LiteralPath
        Non-interpreted path to the file to test.
     
    .PARAMETER Code
        Actual code to test.
 
    .PARAMETER Not
        Reverses the returned logic: A syntax error found returns as $true, an error-free script returns $false.
     
    .EXAMPLE
        PS C:\> Test-ReSyntax .\script.ps1
         
        Verifies the syntax of the file 'script.ps1' in the current path.
    #>

    [OutputType([bool])]
    [CmdletBinding(DefaultParameterSetName = 'path')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'path', Position = 0)]
        [string]
        $Path,

        [Parameter(Mandatory = $true, ParameterSetName = 'literal')]
        [string]
        $LiteralPath,

        [Parameter(Mandatory = $true, ParameterSetName = 'code')]
        [string]
        $Code,

        [switch]
        $Not
    )

    process {
        if ($Code) {
            $result = Read-ReAst -ScriptCode $Code
            return ($result.Errors -as [bool]) -eq $Not
        }
        if ($Path) {
            try { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem -SingleItem }
            catch { return $Not -as [bool] } # as bool to satisfy output type PSSA warnings and unify result type

            $fileItem = Get-Item -LiteralPath $resolvedPath
        }
        if ($LiteralPath) {
            try { $fileItem = Get-Item -LiteralPath $LiteralPath -ErrorAction Stop }
            catch { return $Not -as [bool] }
        }

        $tokens = $null
        $errors = $null
        $null = [System.Management.Automation.Language.Parser]::ParseFile($fileItem.FullName, [ref]$tokens, [ref]$errors)
        ($errors -as [bool]) -eq $Not
    }
}

function Write-ReAstComponent {
    <#
    .SYNOPSIS
        Updates a scriptfile that was read from using Read-ReAstComponent.
     
    .DESCRIPTION
        Updates a scriptfile that was read from using Read-ReAstComponent.
        Automatically picks up the file to update from the scan results.
        Expects the caller to first apply changes on the test results outside of the Refactor module.
 
        This command processes all output in end, to support sane pipeline processing of multiple findings from a single file.
     
    .PARAMETER Components
        Component objects scanned from the file to update.
        Use Read-ReAstComponent.
        Pass all objects from the search in one go (or pipe them into the command)
     
    .PARAMETER PassThru
        Return result objects from the conversion.
        By default, this command updates the files in situ or in the target location (OutPath).
        Whether you use this parameter or not, scan results that were provided input from memory - and are thus not backed by a file - will always be returned as output.
     
    .PARAMETER Backup
        Whether to create a backup of the file before modifying it.
 
    .PARAMETER OutPath
        Folder to which to write the converted scriptfile.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
 
    .EXAMPLE
        PS C:\> Write-ReAstComponent -Components $scriptParts
         
        Writes back the components in $scriptParts, which had previously been generated using Read-ReAstComponent, then had their content modified.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [OutputType([Refactor.Component.ScriptFileConverted])]
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'default')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Refactor.Component.AstResult[]]
        $Components,

        [switch]
        $PassThru,

        [Parameter(ParameterSetName = 'inplace')]
        [switch]
        $Backup,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'path')]
        [PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')]
        [string]
        $OutPath,

        [switch]
        $EnableException
    )
    begin {
        $componentObjects = [System.Collections.ArrayList]@()
        if ($OutPath) {
            $resolvedOutPath = Resolve-PSFPath -Path $OutPath
        }
    }
    process {
        $null = $componentObjects.AddRange($Components)
    }
    end {
        $grouped = $componentObjects | Group-Object { $_.Result.Id }
        foreach ($tokenGroup in $grouped) {
            $scriptFile = $tokenGroup.Group[0].File
            $before = $scriptFile.Content
            $null = $scriptFile.Transform($tokenGroup.Group.Token)
            if (-not $OutPath -and $before -eq $scriptFile.Content) { continue }

            if ($PassThru) {
                [Refactor.Component.ScriptFileConverted]::new($tokenGroup.Group[0].Result)
            }

            #region From File
            if ($scriptFile.FromFile) {
                if ($OutPath) {
                    Invoke-PSFProtectedCommand -Action "Writing updated script to $resolvedOutPath" -Target $scriptFile.Path -ScriptBlock {
                        $scriptfile.WriteTo($resolvedOutPath, "")
                    } -PSCmdlet $PSCmdlet -EnableException $EnableException -Continue
                    continue
                }

                Invoke-PSFProtectedCommand -Action 'Replacing content of script' -Target $scriptFile.Path -ScriptBlock {
                    $scriptfile.Save($Backup.ToBool())
                } -PSCmdlet $PSCmdlet -EnableException $EnableException -Continue
            }
            #endregion From File

            #region From Content
            else {
                if ($OutPath) {
                    Invoke-PSFProtectedCommand -Action "Writing updated script to $resolvedOutPath" -Target $scriptFile.Path -ScriptBlock {
                        $scriptfile.WriteTo($resolvedOutPath, $scriptFile.Path)
                    } -PSCmdlet $PSCmdlet -EnableException $EnableException -Continue
                    continue
                }

                # Since it's already returned once for $PassThru, let's not double up here
                if (-not $PassThru) {
                    [Refactor.Component.ScriptFileConverted]::new($tokenGroup[0].Result)
                }
            }
            #endregion From Content
        }
    }
}

function Clear-ReBreakingChange {
    <#
    .SYNOPSIS
        Removes entire datasets of entries from the list of registered breaking changes.
     
    .DESCRIPTION
        Removes entire datasets of entries from the list of registered breaking changes.
     
    .PARAMETER Module
        The module to unregister.
     
    .PARAMETER Version
        The version of the module to unregister.
        If not specified, ALL versions are unregistered.
     
    .EXAMPLE
        PS C:\> Clear-ReBreakingChange -Module MyModule
         
        Removes all breaking changes of all versions of "MyModule" from the in-memory configuration set.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $Module,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Version]
        $Version
    )

    process {
        if (-not $script:breakingChanges[$Module]) { return }
        if (-not $Version) {
            $script:breakingChanges.Remove($Module)
            return
        }
        $script:breakingChanges[$Module].Remove($Version)
        if ($script:breakingChanges[$Module].Count -lt 1) {
            $script:breakingChanges.Remove($Module)
        }
    }
}

function Get-ReBreakingChange {
    <#
    .SYNOPSIS
        Searches for a breaking change configuration entry that has previously registered.
     
    .DESCRIPTION
        Searches for a breaking change configuration entry that has previously registered.
     
    .PARAMETER Module
        The module to search by.
        Defaults to '*'
     
    .PARAMETER Version
        The version of the module to search for.
        By default, changes for all versions are returned.
     
    .PARAMETER Command
        The affected command to search for.
        Defaults to '*'
     
    .PARAMETER Tags
        Only include changes that contain at least one of the listed tags.
     
    .EXAMPLE
        PS C:\> Get-ReBreakingChange
 
        Returns all registered breaking change configuration entries.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]
        $Module = '*',

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Version]
        $Version,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]
        $Command = '*',

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [AllowEmptyCollection()]
        [string[]]
        $Tags
    )

    process {
        $script:breakingChanges.Values.Values | Write-Output | Where-Object {
            if ($_.Module -notlike $Module) { return }
            if ($_.Command -notlike $Command) { return }
            if ($Version -and $_.Version -ne $Version) { return }
            if ($Tags -and -not ($_.Tags | Where-Object { $_ -in $Tags })) { return }
            $true
        }
    }
}

function Import-ReBreakingChange {
    <#
    .SYNOPSIS
        Imports a set of Breaking Change configurations from file.
     
    .DESCRIPTION
        Imports a set of Breaking Change configurations from file.
        Expects a PowerShell Document File (.psd1)
 
        Example layout of import file:
 
        @{
            MyModule = @{
                '2.0.0' = @{
                    'Get-Something' = @{
                        Description = 'Command was fully redesigned'
                    }
                    'Get-SomethingElse' = @{
                        Parameters @{
                            Param1 = 'Parameter was dropped'
                            Param2 = 'Accepts string only now and will not try to parse custom objects anymore'
                            Param3 = 'Was renamed to Param4'
                        }
                        Labels = @('primary')
                    }
                }
            }
        }
     
    .PARAMETER Path
        Path to the file(s) to import.
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .EXAMPLE
        PS C:\> Import-ReBreakingChange -Path .\mymodule.break.psd1
 
        Imports the mymodule.break.psd1
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'File')]
        [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')]
        [Alias('FullName')]
        [string[]]
        $Path,

        [switch]
        $EnableException
    )

    process {
        foreach ($file in $Path | Resolve-PSFPath) {
            $dataSet = Import-PSFPowerShellDataFile -Path $file

            foreach ($module in $dataSet.Keys) {
                foreach ($version in $dataSet.$module.Keys) {
                    if (-not ($version -as [version])) {
                        Stop-PSFFunction -Message "Invalid Version node $($version) for module $($module). Ensure it is a valid version number, prerelease version notations are not supported!" -EnableException $EnableException -Continue -Cmdlet $PSCmdlet
                    }
                    foreach ($command in $dataSet.$module.$version.Keys) {
                        $commandData = $dataSet.$module.$version.$command

                        $param = @{
                            Module = $module
                            Version = $version
                            Command = $command
                        }
                        if ($commandData.Description) { $param.Description = $commandData.Description }
                        if ($commandData.Parameters) { $param.Parameters = $commandData.Parameters }
                        if ($commandData.Tags) { $param.Tags = $commandData.Tags }
                        
                        Register-ReBreakingChange @param
                    }
                }
            }
        }
    }
}

function Register-ReBreakingChange {
    <#
    .SYNOPSIS
        Register a breaking change.
     
    .DESCRIPTION
        Register a breaking change.
        A breaking change is a definition of a command or its parameters that were broken at a given version of the module.
        This can include tags to classify the breaking change.
     
    .PARAMETER Module
        The name of the module the breaking change occured in.
     
    .PARAMETER Version
        The version of the module in which the breaking change was applied.
     
    .PARAMETER Command
        The command that was changed in a breaking manner.
     
    .PARAMETER Description
        A description to show when reporting the command itself as being broken.
        This is the message shown in the report when finding this breaking change, so make sure it contains actionable information for the user.
     
    .PARAMETER Parameters
        A hashtable containing parameters that were broken, maping parametername to a description of what was changed.
        That description will be shown to the user, so make sure it contains actionable information.
        Defining parameters will cause the command to only generate scan results when the parameter is being used or the total parameters cannot be determined.
        It is possible to assign multiple breaking changes to the same command - one for the command and one for parameters.
     
    .PARAMETER Tags
        Any tags to assign to the breaking change.
        Breaking Change scans can be filtered by tags.
     
    .EXAMPLE
        PS C:\> Register-ReBreakingChange -Module MyModule -Version 2.0.0 -Command Get-Something -Description 'Redesigned command'
 
        Adds a breaking change for the Get-Something command in the module MyModule at version 2.0.0
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $Module,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Version]
        $Version,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $Command,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]
        $Description,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Hashtable]
        $Parameters = @{ },

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Tags = @()
    )

    process {
        if (-not $script:breakingChanges[$Module]) {
            $script:breakingChanges[$Module] = @{ }
        }

        if (-not $script:breakingChanges[$Module][$Version]) {
            $script:breakingChanges[$Module][$Version] = [System.Collections.Generic.List[object]]::new()
        }

        $object = [PSCustomObject]@{
            Module      = $Module
            Version     = $Version
            Command     = $Command
            Description = $Description
            Parameters  = $Parameters
            Tags        = $Tags
        }

        $script:breakingChanges[$Module][$Version].Add($object)
    }
}

function Search-ReBreakingChange {
    <#
    .SYNOPSIS
        Search script files for breaking changes.
     
    .DESCRIPTION
        Search script files for breaking changes.
        Use Import-ReBreakingChange or Register-ReBreakingChange to define which command was broken in what module and version.
     
    .PARAMETER Path
        Path to the file(s) to scan.
     
    .PARAMETER Content
        Script Content to scan.
     
    .PARAMETER Name
        Name of the scanned content
     
    .PARAMETER Module
        The module(s) to scan for.
        This can be either a name (and then use the version definitions from -FromVersion and -ToVersion parameters),
        or a Hashtable with three keys: Name, FromVersion and ToVersion.
        Example inputs:
 
        MyModule
        @{ Name = 'MyModule'; FromVersion = '1.0.0'; ToVersion = '2.0.0' }
     
    .PARAMETER FromVersion
        The version of the module for which the script was written.
     
    .PARAMETER ToVersion
        The version of the module to which the script is being migrated
     
    .PARAMETER Tags
        Only include breaking changes that include one of these tags.
        This allows targeting a specific subset of breaking changes.
     
    .EXAMPLE
        PS C:\> Get-ChildItem -Path C:\scripts -Recurse -Filter *.ps1 | Search-ReBreakingChange -Module Az -FromVersion 5.0 -ToVersion 7.0
         
        Return all breaking changes in all scripts between Az v5.0 and v7.0.
        Requires a breaking change definition file for the Az Modules to be registered, in order to work.
    #>

    [CmdletBinding(DefaultParameterSetName = 'File')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'File')]
        [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')]
        [Alias('FullName')]
        [string[]]
        $Path,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Content')]
        [string]
        $Content,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Content')]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [object[]]
        $Module,

        [Version]
        $FromVersion,

        [Version]
        $ToVersion,

        [string[]]
        $Tags = @()
    )

    begin {
        #region Collect Changes to apply
        $changeObjects = foreach ($moduleItem in $Module) {
            $fromV = $FromVersion
            $toV = $ToVersion
            if ($moduleItem.FromVersion) { $fromV = $moduleItem.FromVersion }
            if ($moduleItem.ToVersion) { $toV = $moduleItem.ToVersion }
            $moduleName = $moduleItem.Name
            if (-not $moduleName) { $moduleName = $moduleItem.ModuleName }
            if (-not $moduleName) { $moduleName = $moduleItem -as [string] }

            if (-not $fromV) { Write-PSFMessage -Level Warning -Message "Unable to identify the starting version from which the module $moduleItem is being migrated! be sure to specify the '-FromVersion' parameter." -Target $moduleItem }
            if (-not $toV) { Write-PSFMessage -Level Warning -Message "Unable to identify the destination version from which the module $moduleItem is being migrated! be sure to specify the '-ToVersion' parameter." -Target $moduleItem }
            if (-not $fromV) { Write-PSFMessage -Level Warning -Message "Unable to identify the name of the module being migrated! Be sure to specify a legitimate name to the '-Module' parameter." -Target $moduleItem }
            if (-not ($fromV -and $toV -and $moduleName)) {
                Stop-PSFFunction -Message "Failed to resolve the migration metadata - provide a module, the source and the destination version number!" -EnableException $true -Cmdlet $PSCmdlet
            }

            Get-ReBreakingChange -Module $moduleName -Tags $Tags | Where-Object {
                $fromV -lt $_.Version -and
                $toV -ge $_.Version
            }
        }
        $changes = @{ }
        foreach ($group in $changeObjects | Group-Object Command) {
            $changes[$group.Name] = $group.Group
        }
        #endregion Collect Changes to apply
    }
    process {
        switch ($PSCmdlet.ParameterSetName) {
            File {
                foreach ($filePath in $Path) {
                    $ast = Read-ReAst -Path $filePath
                    Find-BreakingChange -Ast $ast.Ast -Changes $changes
                }
            }
            Content {
                $ast = Read-ReAst -ScriptCode $Content
                Find-BreakingChange -Ast $ast.Ast -Name $Name -Changes $changes
            }
        }
    }
}

<#
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 {
     
}
#>


Register-PSFTeppScriptblock -Name 'Refactor.AstTypes' -ScriptBlock {
    'Ast'
    'SequencePointAst'
    'ScriptBlockAst'
    'ParamBlockAst'
    'NamedBlockAst'
    'NamedAttributeArgumentAst'
    'AttributeBaseAst'
    'AttributeAst'
    'TypeConstraintAst'
    'ParameterAst'
    'StatementBlockAst'
    'StatementAst'
    'TypeDefinitionAst'
    'UsingStatementAst'
    'FunctionDefinitionAst'
    'IfStatementAst'
    'DataStatementAst'
    'LabeledStatementAst'
    'LoopStatementAst'
    'ForEachStatementAst'
    'ForStatementAst'
    'DoWhileStatementAst'
    'DoUntilStatementAst'
    'WhileStatementAst'
    'SwitchStatementAst'
    'TryStatementAst'
    'TrapStatementAst'
    'BreakStatementAst'
    'ContinueStatementAst'
    'ReturnStatementAst'
    'ExitStatementAst'
    'ThrowStatementAst'
    'PipelineBaseAst'
    'ErrorStatementAst'
    'ChainableAst'
    'PipelineChainAst'
    'PipelineAst'
    'AssignmentStatementAst'
    'CommandBaseAst'
    'CommandAst'
    'CommandExpressionAst'
    'ConfigurationDefinitionAst'
    'DynamicKeywordStatementAst'
    'BlockStatementAst'
    'MemberAst'
    'PropertyMemberAst'
    'FunctionMemberAst'
    'CompilerGeneratedMemberFunctionAst'
    'CatchClauseAst'
    'CommandElementAst'
    'CommandParameterAst'
    'ExpressionAst'
    'ErrorExpressionAst'
    'TernaryExpressionAst'
    'BinaryExpressionAst'
    'UnaryExpressionAst'
    'AttributedExpressionAst'
    'ConvertExpressionAst'
    'MemberExpressionAst'
    'InvokeMemberExpressionAst'
    'BaseCtorInvokeMemberExpressionAst'
    'TypeExpressionAst'
    'VariableExpressionAst'
    'ConstantExpressionAst'
    'StringConstantExpressionAst'
    'ExpandableStringExpressionAst'
    'ScriptBlockExpressionAst'
    'ArrayLiteralAst'
    'HashtableAst'
    'ArrayExpressionAst'
    'ParenExpressionAst'
    'SubExpressionAst'
    'UsingExpressionAst'
    'IndexExpressionAst'
    'RedirectionAst'
    'MergingRedirectionAst'
    'FileRedirectionAst'
    'AssignmentTarget'
}

Register-PSFTeppScriptblock -Name 'Refactor.TokenProvider' -ScriptBlock {
    (Get-ReTokenProvider).Name
}

<#
# 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 = @{ }

# Container for Breaking Change data
$script:breakingChanges = @{ }

$tokenizer = {
    param (
        $Ast
    )

    $astTypes = Get-ReTokenTransformationSet -Type Ast | ForEach-Object TypeName

    $astObjects = Search-ReAst -Ast $Ast -Filter {
        $args[0].GetType().Name -in $astTypes
    }

    foreach ($astObject in $astObjects.Data) {
        [Refactor.AstToken]::new($astObject)
    }
}
$converter = {
    param (
        [Refactor.ScriptToken]
        $Token,

        $Preview
    )

    <#
    The AST Token is special in that it expects the actual changes to be applied not by configuration but manually outside of the process.
    As such it is pointless to use in the full, config-only driven workflow of Convert-ReScriptFile.
    Instead, manually creating the scriptfile object and executing the workflows is the way to go here.
    #>


    # Return changes
    $Token.GetChanges()
}

$parameters = @(
    'TypeName'
)
$param = @{
    Name                = 'Ast'
    TransformIndex      = 'TypeName'
    ParametersMandatory = 'TypeName'
    Parameters          = $parameters
    Tokenizer           = $tokenizer
    Converter           = $converter
}
Register-ReTokenProvider @param

$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

$tokenizer = {
    param (
        $Ast
    )

    $functionAsts = Search-ReAst -Ast $Ast -Filter {
        $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst]
    }

    foreach ($functionAst in $functionAsts.Data) {
        [Refactor.FunctionToken]::new($functionAst)
    }
}
$converter = {
    param (
        [Refactor.ScriptToken]
        $Token,

        $Preview
    )

    $transform = Get-ReTokenTransformationSet -Type Function | Where-Object Name -EQ $Token.Name
    if (-not $transform) { return }

    #region Function Name
    if ($transform.NewName) {
        $startIndex = $Token.Ast.Extent.Text.IndexOf($Token.Ast.Name) + $Token.Ast.Extent.StartOffset
        $Token.AddChange($Token.Ast.Name, $transform.NewName, $startIndex, $null)

        $helpData = $Token.Ast.GetHelpContent()
        $endOffset = $Token.Ast.Body.ParamBlock.Extent.StartOffset
        if (-not $endOffset) { $Token.Ast.Body.DynamicParamBlock.Extent.StartOffset }
        if (-not $endOffset) { $Token.Ast.Body.BeginBlock.Extent.StartOffset }
        if (-not $endOffset) { $Token.Ast.Body.ProcessBlock.Extent.StartOffset }
        if (-not $endOffset) { $Token.Ast.Body.EndBlock.Extent.StartOffset }

        foreach ($example in $helpData.Examples) {
            foreach ($line in $example -split "`n") {
                if ($line -notmatch "\b$($Token.Ast.Name)\b") { continue }
                $lineIndex = $Token.Ast.Extent.Text.Indexof($line)
                $commandIndex = ($line -split "\b$($Token.Ast.Name)\b")[0].Length
                # Hard-Prevent editing in function body.
                # Renaming references, including recursive references, is responsibility of the Command token
                if (($lineIndex + $line.Length) -gt $endOffset) { continue }

                $Token.AddChange($Token.Ast.Name, $transform.NewName, ($lineIndex + $commandIndex), $null)
            }
        }
    }
    #endregion Function Name

    # Return changes
    $Token.GetChanges()
}
$parameters = @(
    'Name'
    'NewName'
)
$param = @{
    Name                = 'Function'
    TransformIndex      = 'Name'
    ParametersMandatory = 'Name'
    Parameters          = $parameters
    Tokenizer           = $tokenizer
    Converter           = $converter
}
Register-ReTokenProvider @param
#endregion Load compiled code