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