Commands/Optimization/Optimizer-ConsolidateAspects.ps1


function PipeScript.Optimizer.ConsolidateAspects {
    <#
    .SYNOPSIS
        Consolidates Code Aspects
    .DESCRIPTION
        Consolidates any ScriptBlockExpressions with the same content into variables.
    .EXAMPLE
        {
            a.txt Template 'abc'
            b.txt Template 'abc'
        } | .>PipeScript
    .EXAMPLE
        {
            aspect function SayHi {
                if (-not $args) { "Hello World"}
                else { $args }
            }
            function Greetings {
                SayHi
                SayHi "hallo Welt"
            }
        } | .>PipeScript
    #>

    param(
    # The ScriptBlock. All aspects used more than once within this ScriptBlock will be consolidated.
    [Parameter(Mandatory,ParameterSetName='ScriptBlock',ValueFromPipeline)]
    [scriptblock]
    $ScriptBlock,
    # The Function Definition. All aspects used more than once within this Function Definition will be consolidated.
    [Parameter(Mandatory,ParameterSetName='FunctionDefinition',ValueFromPipeline)]
    [Management.Automation.Language.FunctionDefinitionAst]
    $FunctionDefinitionAst
    )
    begin {
        $findAspectComment = [Regex]::new('# aspect\p{P}(?<n>\S+)', 'IgnoreCase,RightToLeft', '00:00:01')
    }
    process {
        if ($psCmdlet.ParameterSetName -eq 'FunctionDefinition') {
            $ScriptBlock = [scriptblock]::Create($FunctionDefinitionAst.Body -replace '^\{' -replace '\}$')
        }
        # Find all ScriptBlockExpressions
        $script:FoundFunctionExtent = $null
        # If we are in a function, we can consolidate inner functions.
        $script:CurrentFunctionExtent =
            if ($psCmdlet.ParameterSetName -eq 'FunctionDefinition') {
                $FunctionDefinitionAst
            } else {
                $null
            }
        $allExpressions = @($ScriptBlock | Search-PipeScript -AstCondition {
            param($ast)
            if ($ast -is [Management.Automation.Language.FunctionDefinitionAst] -and -not $script:CurrentFunctionExtent) {
                $script:FoundFunctionExtent = $ast.Extent
            }
            if ($ast -isnot [Management.Automation.Language.ScriptBlockExpressionAst]) { return $false }            
            
            if ($script:FoundFunctionExtent -and 
                ($ast.Extent.StartOffset -ge $script:FoundFunctionExtent.StartOffset) -and 
                ($ast.Extent.EndOffset -lt $script:FoundFunctionExtent.EndOffset)) {
                return $false
            }
            if ($ast.Parent -is [Management.Automation.Language.AttributeAst]) { 
                return $false
            }
            
            if ($ast.Parent -is [Management.Automation.Language.AssignmentStatementAst]) { 
                return $false
            }
            if ($ast.Parent -is [Management.Automation.Language.CommandAst] -and 
                $ast.Parent.CommandElements[0] -ne $ast) { 
                return $false
            }
            return $true
        } -Recurse)
        $scriptBlockExpressions = [Ordered]@{}
        
        foreach ($expression in $allExpressions) {
            # skip any expression in an attribute
            if ($expression.Parent -is [Management.Automation.Language.AttributeAst]) {
                continue
            }
            # and bucket the rest
            $matchingAst = $expression.Result -replace '\s'
            
            if (-not $scriptBlockExpressions["$matchingAst"]) {
                $scriptBlockExpressions["$matchingAst"]  = @($expression.Result)
            } else {
                $scriptBlockExpressions["$matchingAst"]  += @($expression.Result)
            }
        }
        # Any bucket
        $consolidations = [Ordered]@{}
        $consolidatedScriptBlocks = [Ordered]@{}
        foreach ($k in $scriptBlockExpressions.Keys) {
            # with 2 or more values
            if ($scriptBlockExpressions[$k].Length -lt 2) {
                continue
            }
            # is fair game for consolidation
            # (if it's not itself
            if ("$k" -eq ("$ScriptBlock" -replace '\s')) {
                continue
            }
            # or blank)
            if ("$k" -match "^\s{0,}\{\s{0,}\}\s{0,}$") {
                continue
            }
            # Of course we have to figure out what the variable we're consolidating into is called
            $potentialNames = 
                @(foreach ($value in $scriptBlockExpressions[$k]) {
                    $grandParent = $value.Parent.Parent
                    $greatGrandParent = $value.Parent.Parent.Parent
                    # If it's in a hashtable, use the key
                    if ($greatGrandParent -is [Management.Automation.Language.HashtableAst]) {
                        foreach ($kvp in $greatGrandParent.KeyValuePairs) {
                            if ($kvp.Item2 -ne $grandParent) { continue }
                            $kvp.Item1
                        }
                    }
                    # If it's in an assignment, use the left side
                    elseif ($greatGrandParent -is [Management.Automation.Language.AssignmentStatementAst]) {
                        $greatGrandParent.Left -replace '^\$'
                    }
                    # If it's a member invocation
                    elseif ($value.Parent -is [Management.Automation.Language.InvokeMemberExpressionAst]) {
                        # use any preceeding value name
                        @(foreach ($arg in $value.Parent.Arguments) {                            
                            if ($arg -is  [Management.Automation.Language.ScriptBlockExpressionAst] -and $arg.Extent.ToString() -eq "$k") {
                                break
                            } elseif ($arg.Value) {
                                $arg.Value
                            }
                        }) -join '_'
                    }
                    elseif (                        
                        # Otherwise, if the previous comment line is "aspect.Name"
                        $greatGrandParent.Parent -and                         
                        "$($greatGrandParent.Parent)".Substring(0,  $greatGrandParent.Extent.StartOffset - $greatGrandParent.Parent.Extent.StartOffset) -match $findAspectComment
                    ) {
                        # it's the aspect name.
                        $matches.n
                    }
                    else {
                        # Otherwise, we don't know what we'd call it (and cannot consolidate)
                        $null = $null
                    }
                })
            $uniquePotentialNames = @{}
            foreach ($potentialName in $potentialNames) {
                $uniquePotentialNames[$potentialName] = $potentialName
            }                         
            if ($uniquePotentialNames.Count -eq 1) {
                $determinedAspectName = "$(@($uniquePotentialNames.Keys))Aspect"
                $consolidatedScriptBlocks[$determinedAspectName] = $scriptBlockExpressions[$k][0]
                foreach ($scriptExpression in $scriptBlockExpressions[$k]) {
                    $consolidations[$scriptExpression] = $determinedAspectName
                }
            }
        }
        # Turn each of the consolidations into a regex replacement
        $astReplacement = [Ordered]@{}
        # and a bunch of content to prepend.
        foreach ($consolidate in $consolidations.GetEnumerator()) {            
            $astReplacement[$consolidate.Key] = [ScriptBlock]::Create('$' + $($consolidate.Value -replace '^\$' + ([Environment]::NewLine)))
        }
        
        $prepend  = if ($consolidatedScriptBlocks) {
            [scriptblock]::Create("$(@(
                foreach ($consolidate in $consolidatedScriptBlocks.GetEnumerator()) {
                    "`$$($consolidate.Key) = $($consolidatedScriptBlocks[$consolidate.Key])"
                }
            ) -join [Environment]::NewLine)"
)
        }
        $updatedScriptBlock = 
            if ($astReplacement.Count -gt 1) {
                Update-PipeScript -AstReplacement $astReplacement -ScriptBlock $ScriptBlock -Prepend $prepend
            }
            else {
                $ScriptBlock
            }
        switch ($psCmdlet.ParameterSetName) {
            ScriptBlock {
                $updatedScriptBlock
            }
            FunctionDefinition {
                [scriptblock]::Create(
                    @(
                        "$(if ($FunctionDefinitionAst.IsFilter) { "filter"} else { "function"}) $($FunctionDefinitionAst.Name) {"
                        $UpdatedScriptBlock
                        "}"
                    ) -join [Environment]::NewLine
                ).Ast.EndBlock.Statements[0]
            }
        }
    }
}