Functions/GenXdev.Coding.PowerShell.Modules/Measure-UseFullyQualifiedCmdletNames.psm1

<#
.SYNOPSIS
Detects invocations of non fully-qualified cmdlet names
 
.DESCRIPTION
Ensures all cmdlets and functions are called with their fully qualified module name
 
.EXAMPLE
Invoke-ScriptAnalyzer -Path script.ps1 -CustomRulePath ./CustomPSScriptAnalyzerRules
 
.INPUTS
[System.Management.Automation.Language.ScriptBlockAst]
 
.OUTPUTS
[Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]]
 
.NOTES
PSScriptAnalyzer custom rules must follow specific patterns to be recognized
#>

function Measure-UseFullyQualifiedCmdletNames {
    [CmdletBinding()]
    [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]

    Param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.Language.ScriptBlockAst[]]
        $ScriptBlockAst
    )

    Process {
        [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]$results = @()

        try {
            Write-Verbose "Starting analysis of script block for unqualified cmdlet names"

            if ($null -eq $ScriptBlockAst -or $ScriptBlockAst.Count -eq 0) {
                Write-Verbose "No valid ScriptBlockAst provided - exiting"
                return $results
            }

            foreach ($block in $ScriptBlockAst) {
                if ($null -eq $block) { continue }

                Write-Verbose "Searching for command invocations in the script block"
                $commands = @($block.FindAll({
                            param($ast)
                            return $ast -is [System.Management.Automation.Language.CommandAst]
                        }, $true))

                Write-Verbose "Found $($commands.Count) command invocations to analyze"

                foreach ($command in $commands) {
                    if ($null -eq $command) { continue }

                    $commandName = $command.GetCommandName()
                    Write-Verbose "Processing command: $commandName"

                    if ([string]::IsNullOrEmpty($commandName)) {
                        Write-Verbose "Skipping: Unable to get command name"
                        continue
                    }

                    if ($commandName -notmatch '\\') {
                        Write-Verbose "Command '$commandName' is not fully qualified - attempting to resolve"

                        Write-Verbose "Attempting to get command information for '$commandName'"
                        $resolvedCommand = Get-Command -Name $commandName -ErrorAction SilentlyContinue
                        if ($null -eq $resolvedCommand) {
                            Write-Verbose "Get-Command failed, trying ExecutionContext"
                            $resolvedCommand = $ExecutionContext.InvokeCommand.GetCommand($commandName, [System.Management.Automation.CommandTypes]::All)
                        }

                        if ($resolvedCommand) {
                            Write-Verbose "Command resolved as $($resolvedCommand.CommandType) from module $($resolvedCommand.ModuleName)"

                            if (($resolvedCommand.CommandType -eq 'Cmdlet' -or $resolvedCommand.CommandType -eq 'Function') -and
                                $resolvedCommand.ModuleName) {
                                $fullyQualifiedName = "$($resolvedCommand.ModuleName)\$commandName"
                                Write-Verbose "Suggested fully qualified name: $fullyQualifiedName"

                                Write-Verbose "Creating diagnostic record for $commandName"
                                $extent = $command.CommandElements[0].Extent

                                # Get file path from ScriptBlockAst first, then extent
                                $filePath = $block.Extent.File
                                if ([string]::IsNullOrEmpty($filePath)) {
                                    $filePath = $extent.File
                                    if ([string]::IsNullOrEmpty($filePath)) {
                                        $currentAst = $command
                                        while ($null -ne $currentAst.Parent) {
                                            $currentAst = $currentAst.Parent
                                            if (-not [string]::IsNullOrEmpty($currentAst.Extent.File)) {
                                                $filePath = $currentAst.Extent.File
                                                break
                                            }
                                        }
                                    }
                                }

                                # Only proceed if we have a valid file path
                                if ([string]::IsNullOrEmpty($filePath)) {
                                    Write-Verbose "Skipping: Unable to determine file path for command '$commandName'"
                                    continue
                                }

                                $ss = @()
                                $s = Expand-Path "$PSScriptRoot\..\..\..\..\..\modules\GenXdev.Local\ScriptAnalyzerFixes.json" -CreateDirectory
                                $I = 0;
                                if ([io.file]::Exists($s)) {

                                    while ($i++ -lt 20) {
                                        try {
                                            $ss = @(Get-Content $s | ConvertFrom-Json)
                                            break;
                                        }
                                        catch {
                                            Start-Sleep -Milliseconds ([Math]::Round(([Random]::new().NextDouble()) * 1000, 0))
                                            $ss = @()
                                        }
                                    }
                                }
                                $ss += @{
                                    StartLineNumber    = $extent.StartLineNumber
                                    EndLineNumber      = $extent.EndLineNumber
                                    StartColumnNumber  = $extent.StartColumnNumber
                                    EndColumnNumber    = $extent.EndColumnNumber
                                    fullyQualifiedName = $fullyQualifiedName
                                    FilePath           = $filePath
                                }
                                $I = 0;
                                while ($i++ -lt 20) {
                                    try {
                                        $ss | ConvertTo-Json -Depth 99 | Out-File $s -Force
                                        break;
                                    }
                                    catch {
                                        Start-Sleep -Milliseconds ([Math]::Round(([Random]::new().NextDouble()) * 1000, 0))
                                    }
                                }

                                # Create correction extent with the fully qualified command name
                                $correction = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent]::new(
                                    $extent.StartLineNumber,
                                    $extent.EndLineNumber,
                                    $extent.StartColumnNumber,
                                    $extent.EndColumnNumber,
                                    $fullyQualifiedName,
                                    "Use fully qualified command name: $fullyQualifiedName"
                                )

                                # Create corrections array with the correction
                                [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent[]]$corrections = @()
                                if ($null -ne $correction) {
                                    $corrections += $correction
                                }

                                # Create diagnostic record with correct constructor parameters
                                $result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]::new(
                                    "Command '$commandName' should be fully qualified with its module name (e.g., '$fullyQualifiedName').",
                                    $extent,
                                    'UseFullyQualifiedCmdletNames',
                                    'Error',
                                    $filePath,
                                    'UseFullyQualifiedCmdletNames',
                                    $corrections
                                )

                                # Set corrections after creation
                                $result.SuggestedCorrections = $corrections

                                $results += $result
                            }
                            else {
                                Write-Verbose "Skipping: Command is not a cmdlet or function, or has no module name"
                            }
                        }
                        else {
                            Write-Verbose "Skipping: Unable to resolve command '$commandName'"
                        }
                    }
                    else {
                        Write-Verbose "Skipping: Command '$commandName' is already fully qualified"
                    }
                }
            }

            Write-Verbose "Completed analysis of script block with $($results.Count) issues found"
            return $results
        }
        catch {
            Write-Verbose "Error occurred: $($_.Exception.Message)"
            $PSCmdlet.ThrowTerminatingError($PSItem)
        }
    }
}