RVR.ScriptAnalyzerRules.psm1

#Requires -Version 3.0

Function Measure-StoppingAfterThrow {
    <#
    .SYNOPSIS
        A throw command should be followed by a return or exit statement.
    .DESCRIPTION
        A throw command should be followed by a return or exit statement.
        Please see https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/be-careful-with-throw-statements-part-1 for more information.
 
    .EXAMPLE
        Measure-StoppingAfterThrow -ScriptBlockAst $ScriptBlockAst
    .INPUTS
        [System.Management.Automation.Language.ScriptBlockAst]
    .OUTPUTS
        [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]
    .NOTES
        These articles describe the problem:
        https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/be-careful-with-throw-statements-part-1
        https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/be-careful-with-throw-statements-part-2
         
        You can also have a look at the Examplescript.ps1 included in this module.
 
        Found this article to be quite helpfull in explaining the PS Abstract Syntax Tree
        https://blog.ipswitch.com/how-to-take-advantage-of-powershells-abstract-syntax-tree
 
        This article helped me setting up my first PS Script analyzer rule.
        https://mathieubuisson.github.io/create-custom-rule-psscriptanalyzer/
 
        Thanks to all!
         
    #>

    
        [CmdletBinding()]
        [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
        Param
        (
            [Parameter(Mandatory = $True)]
            [ValidateNotNullOrEmpty()]
            [System.Management.Automation.Language.ScriptBlockAst] 
            $ScriptBlockAst
        )
        
        Process {


            <#
                Dirty workaround to ignore the child AST blocks.
                # Why this woraround?
            #>

            if($ScriptBlockAst.Parent){
                # I'm not interested in the childs for analysis
                return
            }
            
            [System.Management.Automation.Language.Ast[]]$throws = $ScriptBlockAst.FindAll({$args[0] -is [System.Management.Automation.Language.ThrowStatementAst]}, $true)

            # Use this array to put the results in. If any found.
            $Results = @()

            foreach($throw in $throws){
                $exit = $throw.parent.Find({$args[0] -is [System.Management.Automation.Language.ExitStatementAst]}, $true)
                $return = $throw.parent.Find({$args[0] -is [System.Management.Automation.Language.ReturnStatementAst]}, $true)
            
            
                #if exit exists and startoffset is greater than than throws end ofset (meaning the exit becomes after the throw)
                if($exit -and ($throw.extent.EndOffset -lt $exit.Extent.StartOffset) ){
                    # "Found exit after throw. Test passed with flying collors!"
                }
                elseif($return -and ($throw.extent.EndOffset -lt $return.Extent.StartOffset)) {
                    # again, looking good
                }
            
                else {
                     $Results += New-Object `
                            -Typename "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord" `
                            -ArgumentList "$((Get-Help $MyInvocation.MyCommand.Name).Description.Text)",$throw.Extent,$PSCmdlet.MyInvocation.InvocationName,Warning,$Null
                }
                
                
            }

            $Results 
        }         
    }
    # Export-ModuleMember -Function Measure-*