Private/New-PSCodeHealthReport.ps1

Function New-PSCodeHealthReport {
<#
.SYNOPSIS
    Creates a new custom object and gives it the TypeName : 'PSCodeHealth.Overall.HealthReport'.
.DESCRIPTION
    Creates a new custom object and gives it the TypeName : 'PSCodeHealth.Overall.HealthReport'.
    This output object contains metrics for the code in all the PowerShell files specified via the Path parameter, uses the function health records specified via the FunctionHealthRecord parameter.
    The value of the TestsPath parameter specifies the location of the tests when calling Pester to generate test coverage information.
 
.PARAMETER ReportTitle
    To specify the title of the health report.
    This is mainly used when generating an HTML report.
 
.PARAMETER AnalyzedPath
    To specify the code path being analyzed.
    This corresponds to the original Path value of Invoke-PSCodeHealth.
 
.PARAMETER Path
    To specify the path of one or more PowerShell file(s) to analyze.
 
.PARAMETER FunctionHealthRecord
    To specify the PSCodeHealth.Function.HealthRecord objects which will be the basis for the report.
 
.PARAMETER TestsPath
    To specify the file or directory where the Pester tests are located.
    If a directory is specified, the directory and all subdirectories will be searched recursively for tests.
 
.PARAMETER TestsResult
    To use an existing Pester tests result object for generating the following metrics :
      - NumberOfTests
      - NumberOfFailedTests
      - FailedTestsDetails
      - NumberOfPassedTests
      - TestsPassRate (%)
      - TestCoverage (%)
      - CommandsMissedTotal
 
.EXAMPLE
    PS C:\> New-PSCodeHealthReport -ReportTitle 'MyTitle' -AnalyzedPath 'C:\Folder' -Path $MyPath -FunctionHealthRecord $FunctionHealthRecords -TestsPath "$MyPath\Tests"
 
    Returns new custom object of the type PSCodeHealth.Overall.HealthReport, containing metrics for the code in all the PowerShell files in $MyPath, using the function health records in $FunctionHealthRecords and running all tests in "$MyPath\Tests" (and its subdirectories) to generate test coverage information.
 
.OUTPUTS
    PSCodeHealth.Overall.HealthReport
 
.NOTES
     
#>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    Param (
        [Parameter(Position=0, Mandatory)]
        [string]$ReportTitle,

        [Parameter(Position=1, Mandatory)]
        [string]$AnalyzedPath,
        
        [Parameter(Position=2, Mandatory)]
        [string[]]$Path,

        [Parameter(Position=3, Mandatory)]
        [AllowNull()]
        [PSTypeName('PSCodeHealth.Function.HealthRecord')]
        [PSCustomObject[]]$FunctionHealthRecord,

        [Parameter(Position=4, Mandatory)]
        [ValidateScript({ Test-Path $_ })]
        [string]$TestsPath,

        [Parameter(Position=5, Mandatory=$False)]
        [PSCustomObject]$TestsResult
    )

    # Getting ScriptAnalyzer findings from PowerShell manifests or data files and adding them to the report
    # because these findings don't show up in the FunctionHealthRecords
    $Psd1Files = $Path | Where-Object { $_ -like "*.psd1" }
    If ( $Psd1Files ) {
        $Psd1ScriptAnalyzerResults = $Psd1Files | ForEach-Object { Invoke-ScriptAnalyzer -Path $_ }

        # Have to do that because even if $Psd1ScriptAnalyzerResults is Null, it adds 1 to the number of items in $AllScriptAnalyzerResults
        If ( $Psd1ScriptAnalyzerResults ) {
            $AllScriptAnalyzerResults = ($FunctionHealthRecord.ScriptAnalyzerResultDetails | Where-Object { $_ }) + $Psd1ScriptAnalyzerResults
        }
        Else {
            $AllScriptAnalyzerResults = ($FunctionHealthRecord.ScriptAnalyzerResultDetails | Where-Object { $_ })
        }
    }
    Else {
        $AllScriptAnalyzerResults = ($FunctionHealthRecord.ScriptAnalyzerResultDetails | Where-Object { $_ })
    }
    $ScriptAnalyzerErrors = $AllScriptAnalyzerResults | Where-Object Severity -EQ 'Error'
    $ScriptAnalyzerWarnings = $AllScriptAnalyzerResults | Where-Object Severity -EQ 'Warning'
    $ScriptAnalyzerInformation = $AllScriptAnalyzerResults | Where-Object Severity -EQ 'Information'

    # Gettings overall test coverage for all code in $Path
    If ( ($PSBoundParameters.ContainsKey('TestsResult')) ) {
        $TestsResult = $PSBoundParameters.TestsResult
    }
    Else {
        $OverallPesterParams = @{
            Script = $TestsPath
            CodeCoverage = $Path
            PassThru = $True
            Strict = $True
            Verbose = $False
            WarningAction = 'SilentlyContinue'
        }

        # Invoke-Pester didn't have the "Show" parameter prior to version 4.x
        $SuppressOutput = If ((Get-Module -Name Pester).Version.Major -lt 4) { @{Quiet = $True} } Else { @{Show = 'None'} }

        $TestsResult = Invoke-Pester @OverallPesterParams @SuppressOutput
    }
    If ( $TestsResult.CodeCoverage ) {
        $CodeCoverage = $TestsResult.CodeCoverage
        $CommandsMissed = $CodeCoverage.NumberOfCommandsMissed
        Write-VerboseOutput -Message "Number of commands found in the function : $($CommandsMissed)"

        $CommandsFound = $CodeCoverage.NumberOfCommandsAnalyzed
        Write-VerboseOutput -Message "Number of commands found in the function : $($CommandsFound)"

        # To prevent any "Attempted to divide by zero" exceptions
        If ( $CommandsFound -ne 0 ) {
            $CommandsExercised = $CodeCoverage.NumberOfCommandsExecuted
            Write-VerboseOutput -Message "Number of commands exercised in the tests : $($CommandsExercised)"
            [System.Double]$CodeCoveragePerCent = [math]::Round(($CommandsExercised / $CommandsFound) * 100, 2)
        }
        Else {
            [System.Double]$CodeCoveragePerCent = 0
        }
    }

    $FailedTestsDetails = If ($TestsResult.FailedCount -gt 0) { New-FailedTestsInfo -TestsResult $TestsResult } Else { $Null }

    $ObjectProperties = [ordered]@{
        'ReportTitle'                   = $ReportTitle
        'ReportDate'                    = Get-Date -Format u
        'AnalyzedPath'                  = $AnalyzedPath
        'Files'                         = $Path.Count
        'Functions'                     = $FunctionHealthRecord.Count
        'LinesOfCodeTotal'              = ($FunctionHealthRecord.LinesOfCode | Measure-Object -Sum).Sum
        'LinesOfCodeAverage'            = [math]::Round(($FunctionHealthRecord.LinesOfCode | Measure-Object -Average).Average, 2)
        'ScriptAnalyzerFindingsTotal'   = ($AllScriptAnalyzerResults | Measure-Object).Count
        'ScriptAnalyzerErrors'          = ($ScriptAnalyzerErrors | Measure-Object).Count
        'ScriptAnalyzerWarnings'        = ($ScriptAnalyzerWarnings | Measure-Object).Count
        'ScriptAnalyzerInformation'     = ($ScriptAnalyzerInformation | Measure-Object).Count
        'ScriptAnalyzerFindingsAverage' = [math]::Round(($FunctionHealthRecord.ScriptAnalyzerFindings | Measure-Object -Average).Average, 2)
        'FunctionsWithoutHelp'          = ($FunctionHealthRecord | Where-Object { -not($_.ContainsHelp) } | Measure-Object).Count
        'NumberOfTests'                 = If ( $TestsResult ) { $TestsResult.TotalCount } Else { 0 }
        'NumberOfFailedTests'           = If ( $TestsResult ) { $TestsResult.FailedCount } Else { 0 }
        'FailedTestsDetails'            = $FailedTestsDetails
        'NumberOfPassedTests'           = If ( $TestsResult ) { $TestsResult.PassedCount } Else { 0 }
        'TestsPassRate'                 = If ($TestsResult.TotalCount) { [math]::Round(($TestsResult.PassedCount / $TestsResult.TotalCount) * 100, 2) } Else { 0 }
        'TestCoverage'                  = $CodeCoveragePerCent
        'CommandsMissedTotal'           = $CommandsMissed
        'ComplexityAverage'             = [math]::Round(($FunctionHealthRecord.Complexity | Measure-Object -Average).Average, 2)
        'ComplexityHighest'             = [math]::Round(($FunctionHealthRecord.Complexity | Measure-Object -Maximum).Maximum, 2)
        'NestingDepthAverage'           = [math]::Round(($FunctionHealthRecord.MaximumNestingDepth | Measure-Object -Average).Average, 2)
        'NestingDepthHighest'           = [math]::Round(($FunctionHealthRecord.MaximumNestingDepth | Measure-Object -Maximum).Maximum, 2)
        'FunctionHealthRecords'         = $FunctionHealthRecord
    }

    $CustomObject = New-Object -TypeName PSObject -Property $ObjectProperties
    $CustomObject.psobject.TypeNames.Insert(0, 'PSCodeHealth.Overall.HealthReport')
    return $CustomObject
}