functions/refactor/Set-PSMDCmdletBinding.ps1

function Set-PSMDCmdletBinding
{
    <#
        .SYNOPSIS
            Adds cmdletbinding attributes in bulk
         
        .DESCRIPTION
            Searches the specified file(s) for functions that ...
            - Do not have a cmdlet binding attribute
            - Do have a param block
            and inserts a cmdletbinding attribute for them.
     
            Will not change files where functions already have this attribute. Will also update internal functions.
         
        .PARAMETER FullName
            The file to process
         
        .PARAMETER DisableCache
            By default, this command caches the results of its execution in the PSFramework result cache.
            This information can then be retrieved for the last command to do so by running Get-PSFResultCache.
            Setting this switch disables the caching of data in the cache.
     
        .PARAMETER Confirm
            If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
         
        .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.
         
        .EXAMPLE
            PS C:\> Get-ChildItem .\functions\*\*.ps1 | Set-PSMDCmdletBinding
     
            Updates all commands in the module to have a cmdletbinding attribute.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $FullName,
        
        [switch]
        $DisableCache
    )
    
    begin
    {
        #region Utility functions
        function Invoke-AstWalk
        {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
            [CmdletBinding()]
            Param (
                $Ast,
                
                [string[]]
                $Command,
                
                [string[]]
                $Name,
                
                [string]
                $NewName,
                
                [bool]
                $IsCommand,
                
                [bool]
                $NoAlias
            )
            
            #Write-PSFMessage -Level Host -Message "Processing $($Ast.Extent.StartLineNumber) | $($Ast.Extent.File) | $($Ast.GetType().FullName)"
            $typeName = $Ast.GetType().FullName
            
            switch ($typeName)
            {
                "System.Management.Automation.Language.FunctionDefinitionAst"
                {
                    #region Has a param block, but no cmdletbinding
                    if (($null -ne $Ast.Body.ParamBlock) -and (-not ($Ast.Body.ParamBlock.Attributes | Where-Object TypeName -Like "CmdletBinding")))
                    {
                        $text = [System.IO.File]::ReadAllText($Ast.Extent.File)
                        
                        $index = $Ast.Body.ParamBlock.Extent.StartOffset
                        while (($index -gt 0) -and ($text.Substring($index, 1) -ne "`n")) { $index = $index - 1 }
                        
                        $indentIndex = $index + 1
                        $indent = $text.Substring($indentIndex, ($Ast.Body.ParamBlock.Extent.StartOffset - $indentIndex))
                        Add-FileReplacement -Path $Ast.Body.ParamBlock.Extent.File -Start $indentIndex -Length ($Ast.Body.ParamBlock.Extent.StartOffset - $indentIndex) -NewContent "$($indent)[CmdletBinding()]`n$($indent)"
                    }
                    #endregion Has a param block, but no cmdletbinding
                    
                    Invoke-AstWalk -Ast $Ast.Body -Command $Command -Name $Name -NewName $NewName -IsCommand $false
                }
                default
                {
                    foreach ($property in $Ast.PSObject.Properties)
                    {
                        if ($property.Name -eq "Parent") { continue }
                        if ($null -eq $property.Value) { continue }
                        
                        if (Get-Member -InputObject $property.Value -Name GetEnumerator -MemberType Method)
                        {
                            foreach ($item in $property.Value)
                            {
                                if ($item.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast")
                                {
                                    Invoke-AstWalk -Ast $item -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand
                                }
                            }
                            continue
                        }
                        
                        if ($property.Value.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast")
                        {
                            Invoke-AstWalk -Ast $property.Value -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand
                        }
                    }
                }
            }
        }
        
        function Add-FileReplacement
        {
            [CmdletBinding()]
            Param (
                [string]
                $Path,
                
                [int]
                $Start,
                
                [int]
                $Length,
                
                [string]
                $NewContent
            )
            Write-PSFMessage -Level Verbose -Message "Change Submitted: $Path | $Start | $Length | $NewContent" -Tag 'update', 'change', 'file'
            
            if (-not $globalFunctionHash.ContainsKey($Path))
            {
                $globalFunctionHash[$Path] = @()
            }
            
            $globalFunctionHash[$Path] += New-Object PSObject -Property @{
                Content        = $NewContent
                Start        = $Start
                Length        = $Length
            }
        }
        
        function Apply-FileReplacement
        {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")]
            [CmdletBinding()]
            Param (
                
            )
            
            foreach ($key in $globalFunctionHash.Keys)
            {
                $value = $globalFunctionHash[$key] | Sort-Object Start
                $content = [System.IO.File]::ReadAllText($key)
                
                $newString = ""
                $currentIndex = 0
                
                foreach ($item in $value)
                {
                    $newString += $content.SubString($currentIndex, ($item.Start - $currentIndex))
                    $newString += $item.Content
                    $currentIndex = $item.Start + $item.Length
                }
                
                $newString += $content.SubString($currentIndex)
                
                [System.IO.File]::WriteAllText($key, $newString)
                #$newString
            }
        }
        
        function Write-Issue
        {
            [CmdletBinding()]
            Param (
                $Extent,
                
                $Data,
                
                [string]
                $Type
            )
            
            New-Object PSObject -Property @{
                Type     = $Type
                Data     = $Data
                File     = $Extent.File
                StartLine = $Extent.StartLineNumber
                Text     = $Extent.Text
            }
        }
        #endregion Utility functions
    }
    process
    {
        foreach ($path in $FullName)
        {
            $globalFunctionHash = @{ }
            
            $tokens = $null
            $parsingError = $null
            $ast = [System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$tokens, [ref]$parsingError)
            
            Write-PSFMessage -Level VeryVerbose -Message "Ensuring Cmdletbinding for all functions in $path" -Tag 'start' -Target $Name
            $issues += Invoke-AstWalk -Ast $ast -Command $Command -Name $Name -NewName $NewName -IsCommand $false
            
            Set-PSFResultCache -InputObject $issues -DisableCache $DisableCache
            if ($PSCmdlet.ShouldProcess($path, "Set CmdletBinding attribute"))
            {
                Apply-FileReplacement
            }
            $issues
        }
    }
}