Public/ConvertTo-SplatExpression.ps1

using namespace Microsoft.PowerShell.EditorServices.Extensions
using namespace System.Collections.Generic
using namespace System.Management.Automation.Language

function ConvertTo-SplatExpression {
    <#
    .EXTERNALHELP EditorServicesCommandSuite-help.xml
    #>

    [CmdletBinding()]
    [EditorCommand(DisplayName='Convert Command to Splat Expression')]
    param(
        [System.Management.Automation.Language.Ast]
        $Ast
    )
    end {
        $Ast = GetAncestorOrThrow $Ast -AstTypeName CommandAst -ErrorContext $PSCmdlet

        $commandName, $elements = $Ast.CommandElements.Where({ $true }, 'Split', 1)

        $splat          = @{}
        $retainedArgs   = [List[Ast]]::new()
        $elementsExtent = $elements.Extent | Join-ScriptExtent
        $elements       = [Queue[Ast]]::new($elements -as [Ast[]])

        # Start building the hash table of named parameters and values
        while ($elements.Count -and ($current = $elements.Dequeue())) {
            if ($current -isnot [CommandParameterAst]) {
                # We don't try to figure out positional arguments, so we keep them in final CommandAst
                $retainedArgs.Add($current)
                continue
            }
            # The while is to loop through consecutive switch parameters.
            while ($current -is [CommandParameterAst]) {
                $lastParam = $current
                if (-not $elements.Count) {
                    $splat.$lastParam = '$true'
                    break
                }

                $current = $elements.Dequeue()

                if ($current -is [CommandParameterAst]) {
                    $splat.$lastParam = '$true'
                } else {
                    $splat.$lastParam = $current
                }
            }
        }

        # Remove the hypen, change to camelCase and add 'Splat'
        $variableName = [regex]::Replace(
            ($commandName.Extent.Text -replace '-'),
            '^[A-Z]',
            { $args[0].Value.ToLower() }) +
            'Splat'

        $sb = [System.Text.StringBuilder]::
            new('${0}' -f $variableName).
            AppendLine(' = @{')

        # All StringBuilder methods return itself so it can be chained. We null the whole scriptblock
        # here so unchained method calls don't add to our output.
        $null = & {
            foreach($pair in $splat.GetEnumerator()) {
                $sb.Append(' ').
                    Append($pair.Key.ParameterName).
                    Append(' = ')

                $isStringExpression = $pair.Value -is [StringConstantExpressionAst] -or
                                      $pair.Value -is [ExpandableStringExpressionAst]

                if ($isStringExpression) {
                    # If kind isn't BareWord then it's already enclosed in quotes.
                    if ('BareWord' -ne $pair.Value.StringConstantType) {
                        $sb.AppendLine($pair.Value.Extent.Text)
                    } else {
                        $enclosure = "'"
                        if ($pair.Value.NestedExpressions) {
                            $enclosure = '"'
                        }

                        $sb.AppendFormat('{0}{1}{0}', @($enclosure, $pair.Value.Value)).
                            AppendLine()
                    }
                # When we handle switch parameters we don't create an AST.
                } elseif ($pair.Value -isnot [Ast]) {
                    $sb.AppendLine($pair.Value)
                } else {
                    $sb.AppendLine($pair.Value.Extent.Text)
                }
            }
            $sb.Append('}')
        }
        $splatText = $sb.ToString()

        # New CommandAst will be `Command @splatvar [PositionalArguments]`
        $newCommandParameters = '@' + $variableName
        if ($retainedArgs) {
            $newCommandParameters += ' ' + ($retainedArgs.Extent.Text -join ' ')
        }

        # Change the command expression first so we don't need to track it's position.
        $elementsExtent | Set-ScriptExtent -Text $newCommandParameters

        # Get the parent PipelineAst so we don't add the splat in the middle of a pipeline.
        $pipeline = $Ast | Find-Ast -Ancestor -First { $PSItem -is [PipelineAst] }

        # Prepend the existing indent.
        $lineText = ($psEditor.GetEditorContext().
            CurrentFile.
            Ast.
            Extent.
            Text -split '\r?\n')[$pipeline.Extent.StartLineNumber - 1]

        $lineIndent  = $lineText -match '^\s*' | ForEach-Object { $matches[0] }
        $splatText   = $lineIndent + (
                       $splatText -split '\r?\n' -join ([Environment]::NewLine + $lineIndent))

        # HACK: Temporary workaround until https://github.com/PowerShell/PowerShellEditorServices/pull/541
        #$splatTarget = ConvertTo-ScriptExtent -Line $pipeline.Extent.StartLineNumber
        $splatTarget = [Microsoft.PowerShell.EditorServices.FullScriptExtent]::new(
            $psEditor.GetEditorContext().CurrentFile,
            [Microsoft.PowerShell.EditorServices.BufferRange]::new(
                $pipeline.Extent.StartLineNumber,
                1,
                $pipeline.Extent.StartLineNumber,
                1))

        $splatTarget | Set-ScriptExtent -Text ($splatText + [Environment]::NewLine)
    }
}