Test-Command.ps1

function Test-Command {
    <#
    .Synopsis
        Test-Command checks commands for consistency.
    .Description
        Test-Command checks commands for consistency.
 
        Test-Command run a series of static analysis rules on your script, and helps you see if there's anything to improve.
 
        It will not run any script, just look at the information about the script, like it's help, command metadata, or the script content itself.
    .Example
        Get-Module ScriptCop | Test-Command
    .Example
        Get-Command -Type Cmdlet | Test-Command
    .Example
        Get-Command Get-Command | Test-Command
    .Link
        about_ScriptCop_rules
    #>

    [CmdletBinding(DefaultParameterSetName='Command')]
    [OutputType('ScriptCopError')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification="Variables assigned for debugging")]
    param(
    # The command or module to test. If the object is not a module info or command
    # info, it will not work.
    [Parameter(ValueFromPipeline=$true,Mandatory=$true,Position=0,ParameterSetName='Command')]
    [ValidateScript({
        if ($_ -is [Management.Automation.CommandInfo]) { return $true }
        if ($_ -is [Management.Automation.PSModuleInfo]) { return $true }
        if ($_ -is [IO.FileInfo]) { return $true }
        if ($_ -is [string]) { return $true }
        throw "Input must either be a command, a module, a file, or a name"
    })]
    $Command,

    # A script block containing functions, for instance: function foo {}.
    # Script Blocks that do not contain functions will be ignored.
    [Parameter(Mandatory=$true,ParameterSetName='ScriptBlock',Position=0)]
    [ScriptBlock]
    $ScriptBlock,

    # The scriptcop 'patrol' (list of rules) to run
    #|MaxLength 255
    #|Options Get-ScriptCopPatrol | Select-Object -ExpandProperty Name
    [string]$Patrol,

    # The name of the rule to run
    #|Options Get-ScriptCopRule | Select-Object -ExpandProperty Name
    [String[]]$Rule,

    # Rules to avoid running.
    #|Options Get-ScriptCopRule | Select-Object -ExpandProperty Name
    [String[]]$ExcludedRule
    )

    begin {
        Set-StrictMode -Off

        $CommandMetaData = @()
        $ModuleMetaData = @()

        function WriteScriptCopError{
            param([switch]$IsModuleError)

            if ($ScriptCopError) {
                foreach ($e in $ScriptCopError) {
                    if (-not $e) { continue }
                    $result = New-Object PSObject -Property @{
                        Rule = $testCmd
                        Problem = $e
                        ItemWithProblem = if ($IsModuleError) { $ModuleInfo} else { $CommandInfo }
                    }
                    $result.psObject.TypeNames.Add("ScriptCopError")
                    $result
                }
            }
        }

        $progressId = Get-Random
    }

    process {


        Write-Progress "Collecting Commands" "$command " -Id $progressId
        if ($psCmdlet.ParameterSetName -eq 'Command') {
            if ($command -is [string]) {
                $cmds = @(Get-Command $command -ErrorAction Silentlycontinue)
            } elseif ($command -is [Management.Automation.PSModuleInfo]) {
                $cmds = @($command.ExportedFunctions.Values) + $command.ExportedCmdlets.Values
                $ModuleMetaData += $command
            } elseif ($command -is [Management.Automation.CommandInfo]) {
                $cmds = @($command)
            } elseif ($command -is [IO.FileInfo]) {
                $cmds = @(Get-Command $command.FullName)
            }
        } elseif ($psBoundParameters.Scriptblock) {
            $functionOnly = Get-FunctionFromScript -ScriptBlock $psBoundParameters.ScriptBlock
            $cmds = @()
            foreach ($f in $functionOnly) {
                . ([ScriptBlock]::Create($f))
                $matched = $f -match "function ((\w+-\w+)|(\w+))"
                if ($matched -and $matches[1]) {
                    $cmds+=Get-Command $matches[1]
                }
            }
        }

        if ($cmds) {
            Write-Progress "Collecting Command Details" " " -Id $progressId
            $c = 0
            foreach ($cmd in $cmds) { # Walk thru each command
                $c++
                $perc = $c * 100 / $cmds.Count
                Write-Progress "Collecting Command Details" "$cmd" -PercentComplete $perc -Id $progressId
                $help = $cmd | Get-Help # and get it's help.
                $CommandMetaData += @{ # Then create a property bag with all of the variations of data different rules need:
                    Command = $cmd #* The command itself
                    Function = $(if ($cmd -is [Management.Automation.FunctionInfo]) { $cmd }) #*Any FunctionInfos
                    Application = $(if ($cmd -is [Management.Automation.ApplicationInfo]) { $cmd }) #*Any Applications
                    ExternalScript = $(if ($cmd -is [Management.Automation.ExternalScriptInfo]) { $cmd }) #*Any ExternalScripts
                    Cmdlet = $(if ($cmd -is [Management.Automation.CmdletInfo]) { $cmd }) #*Any Cmdlets
                    Help = $(if ($help -and $help -isnot [string]) { $help }) #*The Command help
                    Tokens = $( #*The Script Block Tokens
                        if ($cmd -is [Management.Automation.FunctionInfo]) {
                            [Management.Automation.PSParser]::Tokenize("function $($cmd.Name) {$($cmd.definition)}",[ref]$null)
                        } elseif ($cmd -is [Management.Automation.ExternalScriptInfo]) {
                            [Management.Automation.PSParser]::Tokenize($cmd.scriptcontents,[ref]$null)
                        }
                    )
                    Text = $( #* The script contents.
                        if ($cmd -is [Management.Automation.FunctionInfo]) {
                            "function $($cmd.Name) {$($cmd.definition)}"
                        } elseif ($cmd -is [Management.Automation.ExternalScriptInfo]) {
                            $cmd.scriptcontents
                        }
                    )
                }
            }
            Write-Progress "Collecting Command Details" " " -ID $progressId
        }
    }

    end {
        Write-Progress 'Filtering Rules' ' ' -Id $progressId

        $currentRules = @{} + $script:ScriptCopRules

        # Create a filter to see if the rule was included.
        $RuleNameMatch = {
            $Rule -contains $_.Name -or
            $Rule -contains $_.Name.Replace(".ps1","")
        }

        if ($Rule) { # If a whitelist was provided, check each rule against the whitelist.
            $currentRules.TestCommandInfo = @($currentRules.TestCommandInfo | Where-Object $RuleNameMatch)
            $currentRules.TestCmdletInfo = @($currentRules.TestCmdletInfo | Where-Object $RuleNameMatch)
            $currentRules.TestScriptInfo = @($currentRules.TestScriptInfo | Where-Object $RuleNameMatch)
            $currentRules.TestFunctionInfo = @($currentRules.TestFunctionInfo | Where-Object $RuleNameMatch)
            $currentRules.TestApplicationInfo=  @($currentRules.TestApplicationInfo | Where-Object $RuleNameMatch)
            $currentRules.TestModuleInfo = @($currentRules.TestModuleInfo | Where-Object $RuleNameMatch)
            $currentRules.TestScriptToken = @($currentRules.TestScriptToken | Where-Object $RuleNameMatch)
            $currentRules.TestHelpContent = @($currentRules.TestHelpContent | Where-Object $RuleNameMatch)
        }

        # Create a filter to see if the rule was excluded.
        $ExcludedRuleNotMatch = {
            $ExcludedRule -notcontains $_.Name -and
            $ExcludedRule -notcontains $_.Name.Replace(".ps1","")
        }

        if ($ExcludedRule) { # If a blacklist was provided, check each rule against the blacklist.
            $currentRules.TestCommandInfo = @($currentRules.TestCommandInfo | Where-Object $ExcludedRuleNotMatch)
            $currentRules.TestCmdletInfo = @($currentRules.TestCmdletInfo | Where-Object $ExcludedRuleNotMatch)
            $currentRules.TestScriptInfo = @($currentRules.TestScriptInfo | Where-Object $ExcludedRuleNotMatch)
            $currentRules.TestFunctionInfo = @($currentRules.TestFunctionInfo | Where-Object $ExcludedRuleNotMatch)
            $currentRules.TestApplicationInfo=  @($currentRules.TestApplicationInfo | Where-Object $ExcludedRuleNotMatch)
            $currentRules.TestModuleInfo = @($currentRules.TestModuleInfo | Where-Object $ExcludedRuleNotMatch)
            $currentRules.TestScriptToken = @($currentRules.TestScriptToken | Where-Object $ExcludedRuleNotMatch)
            $currentRules.TestHelpContent = @($currentRules.TestHelpContent | Where-Object $ExcludedRuleNotMatch)
        }

        if ($patrol) { # If a "Patrol" was provided
            $commandRules = Get-ScriptCopPatrol -Name $patrol | # Find the command
                Select-Object -ExpandProperty CommandRule -ErrorAction SilentlyContinue
            $moduleRules = Get-ScriptCopPatrol -Name $patrol | # and module rules associated with it.
                Select-Object -ExpandProperty ModuleRule -ErrorAction SilentlyContinue

            # Create a whitelist for commands
            $patrolCommandMatch = $RuleNameMatch = {
                $commandRules -contains $_.Name -or
                $commandRules -contains $_.Name.Replace(".ps1","")
            }

            # and for modules
            $patrolModuleMatch = $RuleNameMatch = {
                $moduleRules -contains $_.Name -or
                $moduleRules -contains $_.Name.Replace(".ps1","")
            }

            # Check all rules against the whitelist.
            $currentRules.TestCommandInfo = @($currentRules.TestCommandInfo | Where-Object $patrolCommandMatch)
            $currentRules.TestCmdletInfo = @($currentRules.TestCmdletInfo | Where-Object $patrolCommandMatch)
            $currentRules.TestScriptInfo = @($currentRules.TestScriptInfo | Where-Object $patrolCommandMatch)
            $currentRules.TestFunctionInfo = @($currentRules.TestFunctionInfo | Where-Object $patrolCommandMatch)
            $currentRules.TestApplicationInfo=  @($currentRules.TestApplicationInfo | Where-Object $patrolCommandMatch)
            $currentRules.TestModuleInfo = @($currentRules.TestModuleInfo | Where-Object $patrolModuleMatch)
            $currentRules.TestScriptToken = @($currentRules.TestScriptToken | Where-Object $patrolCommandMatch)
            $currentRules.TestHelpContent = @($currentRules.TestHelpContent | Where-Object $patrolCommandMatch)
        }

        $moduleScriptBlocks = @{}


        # Create a script block to skip rules if [Diagnostics.CodeAnalysis.SuppressMessageAttribute] is
        # found on the command/module.
        $whereSuppressMessage = {
            $_ -is [Diagnostics.CodeAnalysis.SuppressMessageAttribute] -and $testCmd -like $_.Category
        }

        $WhereNotExcludedByAttribute = {
            $in = $_
            -not (
                $in.Command.ScriptBlock.Attributes | # If the command's script block attributes say so,
                    Where-Object $whereSuppressMessage # skip it.
            ) -and -not $(
                if (-not $moduleScriptBlocks[$_]) { # Cache the module script block (just in this run)
                    $moduleScriptBlocks[$_] = try { [ScriptBlock]::Create($in.Command.Module.Definition)} catch {$null}
                }
                $definitionScriptBlock = $moduleScriptBlocks[$_] # If the module's script block attributes say so
                $definitionScriptBlock.Attributes | Where-Object $whereSuppressMessage # skip it.
            )
        }

        Write-Progress "Running ScriptCop" "Validating Modules" -Id $ProgressId
        if ($currentRules.TestModuleInfo) { # If there were module-wide rules
            $c = 0
            $ruleCount = @($currentRules.TestModuleInfo).Count
            foreach ($testCmd in $currentRules.TestModuleInfo){
                $c++
                $perc  = $c * 100 / $ruleCount
                Write-Progress "Running ScriptCop" "Validating Modules - $($testCmd.Name)" -PercentComplete $perc -Id $ProgressId
                if ($scriptCopError) {$scriptCopError = $null }
                $ModuleMetaData | # Run them
                    Where-Object { # unless the module says it doesn't want to.
                        if (-not $moduleScriptBlocks[$_]) {
                            $moduleScriptBlocks[$_] = try { [ScriptBlock]::Create($_.Definition)} catch {$null}
                        }
                        $definitionScriptBlock = $moduleScriptBlocks[$_]
                        -not ($definitionScriptBlock.Attributes | Where-Object $whereSuppressMessage)
                    } |
                    ForEach-Object {
                        $moduleInfo = $_
                        $null = $_ |
                            & $testCmd -ErrorAction SilentlyContinue -ErrorVariable ScriptCopError

                        WriteScriptCopError -IsModuleError
                    }
            }
        }

        #region Validating Commands
        Write-Progress "Running ScriptCop" "Validating Command Metadata" -Id $ProgressId
        if ($currentRules.TestCommandInfo) { # If there were CommandInfo rules
            $c = 0
            $ruleCount = @($currentRules.TestCommandInfo).Count
            foreach ($testCmd in $currentRules.TestCommandInfo){ # run them on each command
                $c++
                $perc  = $c * 100 / $RuleCount
                Write-Progress "Running ScriptCop" "Validating Command Metadata - $($testCmd.Name)" -Id $ProgressId -PercentComplete $perc
                if ($scriptCopError) {$scriptCopError = $null }
                $CommandMetaData |
                    Where-Object { $_.Command } |
                    Where-Object $WhereNotExcludedByAttribute | # (unless they should be skipped).
                    ForEach-Object {
                        $commandInfo = $_.Command
                        $null = $commandInfo |
                            & $testCmd -ErrorAction SilentlyContinue -ErrorVariable ScriptCopError

                        WriteScriptCopError
                    }


            }
        }
        #endregion Validating Commands

        #region Validating Cmdlets
        Write-Progress "Running ScriptCop" "Validating Cmdlet Metadata" -Id $ProgressId

        if ($currentRules.TestCmdletInfo) { # If there were CmdletInfo rules
            $c = 0
            $ruleCount = @($currentRules.TestCmdletInfo).Count
            foreach ($testCmd in $currentRules.TestCmdletInfo){ # run them on each Cmdlet
                $c++
                $perc  = $c * 100 / $ruleCount
                Write-Progress "Running ScriptCop" "Validating Cmdlet Metadata - $($testCmd.Name)" -Id $ProgressId -PercentComplete $perc
                if ($scriptCopError) {$scriptCopError = $null }
                $CmdletMetaData |
                    Where-Object {
                        $_.Cmdlet
                    } |
                    Where-Object $WhereNotExcludedByAttribute | # (unless they should be skipped).
                    ForEach-Object {
                        $commandInfo = $_.Cmdlet
                        $null = $commandInfo |
                            & $testCmd -ErrorAction SilentlyContinue -ErrorVariable ScriptCopError

                        WriteScriptCopError
                    }
            }
        }

        #endregion Validating Cmdlets


        #region Validating Functions
        Write-Progress "Running ScriptCop" "Validating Functions" -Id $ProgressId

        if ($currentRules.TestFunctionInfo) { # If there are function rules
            $c = 0
            $ruleCount = @($currentRules.TestFunctionInfo).Count
            foreach ($testCmd in $currentRules.TestFunctionInfo){ # run them on each function
                $c++
                $perc  = $c * 100 / $ruleCount
                Write-Progress "Running ScriptCop" "Validating Function Metadata - $($testCmd.Name)" -Id $ProgressId -PercentComplete $perc
                if ($scriptCopError) {$scriptCopError = $null }
                $CommandMetaData |
                    Where-Object {
                        $_.Function
                    } |
                    Where-Object $WhereNotExcludedByAttribute | # (unless they should be skipped).
                    ForEach-Object {
                        $commandInfo = $_.Function
                        $null = $commandInfo |
                            & $testCmd -ErrorAction SilentlyContinue -ErrorVariable ScriptCopError

                        WriteScriptCopError
                    }
            }
        }

        #endregion Validating Functions

        #region Validating Applications
        Write-Progress "Running ScriptCop" "Validating Applications Metadata" -Id $ProgressId

        if ($currentRules.TestApplicationInfo) { # If there are application rules
            $c = 0
            $ruleCount = @($currentRules.TestApplicationInfo).Count
            foreach ($testCmd in $currentRules.TestApplicationInfo){ # run them on each application.
                $c++
                $perc  = $c * 100 / $ruleCount
                Write-Progress "Running ScriptCop" "Validating Applications Metadata - $($testCmd.Name)" -Id $ProgressId -PercentComplete $perc
                if ($scriptCopError) {$scriptCopError = $null }
                $CommandMetaData |
                    Where-Object {
                        $_.Application
                    } |
                    ForEach-Object {
                        $commandInfo = $_.Application
                        $null = $commandInfo |
                            & $testCmd -ErrorAction SilentlyContinue -ErrorVariable ScriptCopError

                        WriteScriptCopError
                    }



            }
        }

        #endregion Validating Applications

        #region Validating Scripts
        Write-Progress "Running ScriptCop" "Validating Script Metadata" -Id $ProgressId

        if ($currentRules.TestScriptInfo) { # If there were ExternalScript rules
            $c = 0
            $ruleCount = @($currentRules.TestScriptInfo).Count
            foreach ($testCmd in $currentRules.TestScriptInfo){ # run them on each external script
                $c++
                $perc  = $c * 100 / $ruleCount
                Write-Progress "Running ScriptCop" "Validating Script Metadata - $($testCmd.Name)" -Id $ProgressId -PercentComplete $perc
                if ($scriptCopError) {$scriptCopError = $null }
                $CommandMetaData |
                    Where-Object {  $_.Script } |
                    Where-Object $WhereNotExcludedByAttribute | # (unless they should be skipped).
                    ForEach-Object {
                        $commandInfo = $_.Script
                        $null = $commandInfo |
                            & $testCmd -ErrorAction SilentlyContinue -ErrorVariable ScriptCopError

                        WriteScriptCopError
                    }
            }
        }

        #endregion Validating Scripts

        #region Validating Help
        Write-Progress "Running ScriptCop" "Validating Help" -Id $ProgressId

        if ($currentRules.TestHelpContent) { # If there were Help rules
            $c = 0
            $ruleCount = @($currentRules.TestHelpContent).Count
            foreach ($testCmd in $currentRules.TestHelpContent){ # run them on each command's help
                $c++
                $perc  = $c * 100 / $ruleCount
                Write-Progress "Running ScriptCop" "Validating Help - $($testCmd.Name)" -Id $ProgressId -PercentComplete $perc
                if ($scriptCopError) {$scriptCopError = $null }
                $CommandMetaData |
                    Where-Object $WhereNotExcludedByAttribute | # (unless they should be skipped).
                    ForEach-Object {
                        $commandInfo = $_.Command
                        $null = & $testCmd -HelpCommand $_.Command -HelpContent $_.Help -ErrorAction SilentlyContinue -ErrorVariable ScriptCopError
                        WriteScriptCopError
                    }

            }
        }

        #endregion Validating Help

        #region Validating Tokens
        Write-Progress "Running ScriptCop" "Validating Tokens" -Id $ProgressId

        if ($currentRules.TestScriptToken) { # If there were token-based rules
            $c = 0
            $ruleCount = @($currentRules.TestScriptToken).Count
            foreach ($testCmd in $currentRules.TestScriptToken){ # Run them on each file's tokens
                $c++
                $perc  = $c * 100 / $ruleCount
                Write-Progress "Running ScriptCop" "Validating Tokens - $($testCmd.Name)" -Id $ProgressId -PercentComplete $perc
                if ($scriptCopError) {$scriptCopError = $null }
                $CommandMetaData |
                    Where-Object $WhereNotExcludedByAttribute | # (unless they should be skipped).
                    ForEach-Object {
                        $in = $_
                        $commandInfo = $_.Command
                        if ($_.Tokens -and $_.Text) {
                            $null = & $testCmd -ScriptTokenCommand $in.Command -ScriptToken $in.Tokens -ScriptText "$($in.Text)" -ErrorAction SilentlyContinue -ErrorVariable ScriptCopError
                            WriteScriptCopError
                        }

                    }

            }
        }

        #endregion Validating Tokens

        Write-Progress "Running ScriptCop" " " -Id $ProgressId -Completed
    }
}