Unpacking Software Livestream

Join our monthly Unpacking Software livestream to hear about the latest news, chat and opinion on packaging, software deployment and lifecycle management!

Learn More

Chocolatey Product Spotlight

Join the Chocolatey Team on our regular monthly stream where we put a spotlight on the most recent Chocolatey product releases. You'll have a chance to have your questions answered in a live Ask Me Anything format.

Learn More

Chocolatey Coding Livestream

Join us for the Chocolatey Coding Livestream, where members of our team dive into the heart of open source development by coding live on various Chocolatey projects. Tune in to witness real-time coding, ask questions, and gain insights into the world of package management. Don't miss this opportunity to engage with our team and contribute to the future of Chocolatey!

Learn More

Calling All Chocolatiers! Whipping Up Windows Automation with Chocolatey Central Management

Webinar from
Wednesday, 17 January 2024

We are delighted to announce the release of Chocolatey Central Management v0.12.0, featuring seamless Deployment Plan creation, time-saving duplications, insightful Group Details, an upgraded Dashboard, bug fixes, user interface polishing, and refined documentation. As an added bonus we'll have members of our Solutions Engineering team on-hand to dive into some interesting ways you can leverage the new features available!

Watch On-Demand
Chocolatey Community Coffee Break

Join the Chocolatey Team as we discuss all things Community, what we do, how you can get involved and answer your Chocolatey questions.

Watch The Replays
Chocolatey and Intune Overview

Webinar Replay from
Wednesday, 30 March 2022

At Chocolatey Software we strive for simple, and teaching others. Let us teach you just how simple it could be to keep your 3rd party applications updated across your devices, all with Intune!

Watch On-Demand
Chocolatey For Business. In Azure. In One Click.

Livestream from
Thursday, 9 June 2022

Join James and Josh to show you how you can get the Chocolatey For Business recommended infrastructure and workflow, created, in Azure, in around 20 minutes.

Watch On-Demand
The Future of Chocolatey CLI

Livestream from
Thursday, 04 August 2022

Join Paul and Gary to hear more about the plans for the Chocolatey CLI in the not so distant future. We'll talk about some cool new features, long term asks from Customers and Community and how you can get involved!

Watch On-Demand
Hacktoberfest Tuesdays 2022

Livestreams from
October 2022

For Hacktoberfest, Chocolatey ran a livestream every Tuesday! Re-watch Cory, James, Gary, and Rain as they share knowledge on how to contribute to open-source projects such as Chocolatey CLI.

Watch On-Demand

Downloads:

5,078

Downloads of v 0.2.26:

4,755

Last Update:

11 May 2018

Package Maintainer(s):

Software Author(s):

  • Mathieu Buisson

Tags:

admin powershell module code analysis health report

PSCodeHealth (PowerShell Module)

  • 1
  • 2
  • 3

0.2.26 | Updated: 11 May 2018

Downloads:

5,078

Downloads of v 0.2.26:

4,755

Maintainer(s):

Software Author(s):

  • Mathieu Buisson

PSCodeHealth (PowerShell Module) 0.2.26

Legal Disclaimer: Neither this package nor Chocolatey Software, Inc. are affiliated with or endorsed by Mathieu Buisson. The inclusion of Mathieu Buisson trademark(s), if any, upon this webpage is solely to identify Mathieu Buisson goods or services and not for commercial purposes.

  • 1
  • 2
  • 3

This Package Contains an Exempted Check

Not All Tests Have Passed


Validation Testing Passed


Verification Testing Exemption:

PS 5 is required for this module and it throws an exception if it's not installed.

Details

Scan Testing Successful:

No detections found in any package files

Details
Learn More

Deployment Method: Individual Install, Upgrade, & Uninstall

To install PSCodeHealth (PowerShell Module), run the following command from the command line or from PowerShell:

>

To upgrade PSCodeHealth (PowerShell Module), run the following command from the command line or from PowerShell:

>

To uninstall PSCodeHealth (PowerShell Module), run the following command from the command line or from PowerShell:

>

Deployment Method:

NOTE

This applies to both open source and commercial editions of Chocolatey.

1. Enter Your Internal Repository Url

(this should look similar to https://community.chocolatey.org/api/v2/)


2. Setup Your Environment

1. Ensure you are set for organizational deployment

Please see the organizational deployment guide

2. Get the package into your environment

  • Open Source or Commercial:
    • Proxy Repository - Create a proxy nuget repository on Nexus, Artifactory Pro, or a proxy Chocolatey repository on ProGet. Point your upstream to https://community.chocolatey.org/api/v2/. Packages cache on first access automatically. Make sure your choco clients are using your proxy repository as a source and NOT the default community repository. See source command for more information.
    • You can also just download the package and push it to a repository Download

3. Copy Your Script

choco upgrade pscodehealth -y --source="'INTERNAL REPO URL'" [other options]

See options you can pass to upgrade.

See best practices for scripting.

Add this to a PowerShell script or use a Batch script with tools and in places where you are calling directly to Chocolatey. If you are integrating, keep in mind enhanced exit codes.

If you do use a PowerShell script, use the following to ensure bad exit codes are shown as failures:


choco upgrade pscodehealth -y --source="'INTERNAL REPO URL'" 
$exitCode = $LASTEXITCODE

Write-Verbose "Exit code was $exitCode"
$validExitCodes = @(0, 1605, 1614, 1641, 3010)
if ($validExitCodes -contains $exitCode) {
  Exit 0
}

Exit $exitCode

- name: Install pscodehealth
  win_chocolatey:
    name: pscodehealth
    version: '0.2.26'
    source: INTERNAL REPO URL
    state: present

See docs at https://docs.ansible.com/ansible/latest/modules/win_chocolatey_module.html.


chocolatey_package 'pscodehealth' do
  action    :install
  source   'INTERNAL REPO URL'
  version  '0.2.26'
end

See docs at https://docs.chef.io/resource_chocolatey_package.html.


cChocoPackageInstaller pscodehealth
{
    Name     = "pscodehealth"
    Version  = "0.2.26"
    Source   = "INTERNAL REPO URL"
}

Requires cChoco DSC Resource. See docs at https://github.com/chocolatey/cChoco.


package { 'pscodehealth':
  ensure   => '0.2.26',
  provider => 'chocolatey',
  source   => 'INTERNAL REPO URL',
}

Requires Puppet Chocolatey Provider module. See docs at https://forge.puppet.com/puppetlabs/chocolatey.


4. If applicable - Chocolatey configuration/installation

See infrastructure management matrix for Chocolatey configuration elements and examples.

Package Approved

This package was approved by moderator flcdrg on 13 May 2018.

Description

PSCodeHealth allows you to measure the quality and maintainability of your PowerShell code, based on a variety of metrics related to:

  • Code length
  • Code complexity
  • Code smells, styling issues and violations of best practices
  • Tests and test coverage
  • Comment-based help

It can allow you to ensure that your code is compliant with metrics goals (quality gates). You can use the default (built-in) compliance rules, and you can also customize some (or all) compliance rules to fit your goals.

These features can be leveraged from within your PowerShell release pipeline.

PSCodeHealth can also generate a highly visual HTML report so that you can interpret the results at a glance, and easily share them.

NOTE: This is an automatically updated package. If you find it is out of date by more than a week, please contact the maintainer(s) and let them know the package is no longer updating correctly.


tools\.skipAutoUninstaller
 
tools\chocolateyBeforeModify.ps1
$ErrorActionPreference = 'Stop'

$moduleName = $env:ChocolateyPackageName      # this could be different from package name
Remove-Module -Name $moduleName -Force -ErrorAction SilentlyContinue
tools\chocolateyInstall.ps1
$ErrorActionPreference = 'Stop'

$toolsDir         = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
$moduleName       = 'PSCodeHealth'  # this may be different from the package name and different case

if ($PSVersionTable.PSVersion.Major -lt 5) {
    throw "PSCodeHealth module requires a minimum of PowerShell 5."
}

# module may already be installed outside of Chocolatey
Remove-Module -Name $moduleName -Force -ErrorAction SilentlyContinue

$manifestFile = Join-Path -Path $toolsDir -ChildPath "$moduleName\$moduleName.psd1"
$manifest     = Test-ModuleManifest -Path $manifestFile -WarningAction Ignore -ErrorAction Stop
$sourcePath = Join-Path -Path $toolsDir -ChildPath "$modulename\*"
$destPath = Join-Path -Path $env:ProgramFiles -ChildPath "WindowsPowerShell\Modules\$moduleName\$($manifest.Version.ToString())"

Write-Verbose "Creating destination directory '$destPath' for module."
New-Item -Path $destPath -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null

Write-Verbose "Moving '$moduleName' files from '$sourcePath' to '$destPath'."
Move-Item -Path $sourcePath -Destination $destPath -Force
tools\chocolateyUninstall.ps1
$ErrorActionPreference = 'Stop'

$moduleName = $env:ChocolateyPackageName
$sourcePath = Join-Path -Path $env:ProgramFiles -ChildPath "WindowsPowerShell\Modules\$moduleName"

Write-Verbose "Removing all version of '$moduleName' from '$sourcePath'."
Remove-Item -Path $sourcePath -Recurse -Force -ErrorAction SilentlyContinue

if ($PSVersionTable.PSVersion.Major -lt 4) {
    $modulePaths = [Environment]::GetEnvironmentVariable('PSModulePath', 'Machine') -split ';'

    Write-Verbose "Removing '$sourcePath' from PSModulePath."
    $newModulePath = $modulePaths | Where-Object { $_ -ne $sourcePath }

    [Environment]::SetEnvironmentVariable('PSModulePath', $newModulePath, 'Machine')
    $env:PSModulePath = $newModulePath
}
tools\LICENSE.txt
From: https://github.com/MathieuBuisson/PSCodeHealth/blob/master/LICENSE.md

LICENSE

MIT License

Copyright (c) 2017 Mathieu BUISSON

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
tools\PSCodeHealth\Assets\HealthReport.css
 
tools\PSCodeHealth\Assets\HealthReport.html
 
tools\PSCodeHealth\Assets\HealthReport.js
Chart.pluginService.register({
            afterUpdate: function (chart) {
                if (chart.config.options.elements.center) {
                    var helpers = Chart.helpers;
                    var centerConfig = chart.config.options.elements.center;
                    var globalConfig = Chart.defaults.global;
                    var ctx = chart.chart.ctx;

                    var fontStyle = helpers.getValueOrDefault(centerConfig.fontStyle, globalConfig.defaultFontStyle);
                    var fontFamily = helpers.getValueOrDefault(centerConfig.fontFamily, globalConfig.defaultFontFamily);

                    // Figure out the best font size, if one is not specified
                    ctx.save();
                    var fontSize = helpers.getValueOrDefault(centerConfig.minFontSize, 12);
                    var maxFontSize = helpers.getValueOrDefault(centerConfig.maxFontSize, 55);
                    var maxText = helpers.getValueOrDefault(centerConfig.maxText, centerConfig.text);

                    do {
                        ctx.font = helpers.fontString(fontSize, fontStyle, fontFamily);
                        var textWidth = ctx.measureText(maxText).width;

                        // Check if it fits, is within configured limits and that we are not simply toggling back and forth
                        if (textWidth < chart.innerRadius * 2 && fontSize < maxFontSize)
                            fontSize += 1;
                        else {
                            // Reverse last step
                            fontSize -= 1;
                            break;
                        }
                    } while (true);
                    ctx.restore();

                    // save properties
                    chart.center = {
                        font: helpers.fontString(fontSize, fontStyle, fontFamily),
                        fillStyle: helpers.getValueOrDefault(centerConfig.fontColor, globalConfig.defaultFontColor)
                    };
                }
            },
            afterDraw: function (chart) {
                if (chart.center) {
                    var centerConfig = chart.config.options.elements.center;
                    var ctx = chart.chart.ctx;

                    ctx.save();
                    ctx.font = chart.center.font;
                    ctx.fillStyle = chart.center.fillStyle;
                    ctx.textAlign = 'center';
                    ctx.textBaseline = 'middle';
                    var centerX = (chart.chartArea.left + chart.chartArea.right) / 2;
                    var centerY = (chart.chartArea.top + chart.chartArea.bottom) / 2;
                    ctx.fillText(centerConfig.text, centerX, centerY);
                    ctx.restore();
                }
            },
        });

        function createTestPassRateChart(ctx) {
            var data = {
                labels: ["Pass","Fail"],
                datasets: [
                {
                    data: [{NUMBER_OF_PASSED_TESTS},{NUMBER_OF_FAILED_TESTS}],
                    backgroundColor: ["#a3d48d","#d59595"],
                    hoverBackgroundColor: ["#a3d48d","#d59595"]
                }]
            };
            var newTestPassRateChart = new Chart(ctx, {
                type: 'doughnut',
                data: data,
                options: {
                    cutoutPercentage: 64,
                    legend: { position: 'top' },
                    elements: {
                        center: {
                            // This evaluates the max length of the text
                            maxText: '99.99%',
                            text: '{TESTS_PASS_RATE}%',
                            fontColor: '#a3d48d',
                            fontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
                            fontStyle: 'normal'
                        }
                    }
                }
            });
        }
        var testPassRateCharts = $(".testPassRateChart");
        for (var i = 0; i < testPassRateCharts.length; i++) {
            createTestPassRateChart(testPassRateCharts[i]);
        }

        function createTestCoverageChart(ctx) {
            var data = {
                labels: ["Covered","Missed"],
                datasets: [
                {
                    data: [{TEST_COVERAGE},{CODE_NOT_COVERED}],
                    backgroundColor: ["#a3d48d","#d59595"],
                    hoverBackgroundColor: ["#a3d48d","#d59595"]
                }]
            };
            var newTestCoverageChart = new Chart(ctx, {
                type: 'doughnut',
                data: data,
                options: {
                    cutoutPercentage: 64,
                    legend: { position: 'top' },
                    elements: {
                        center: {
                            // This evaluates the max length of the text
                            maxText: '99.99%',
                            text: '{TEST_COVERAGE}%',
                            fontColor: '#d59595',
                            fontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
                            fontStyle: 'normal'
                        }
                    }
                }
            });
        }
        var testCoverageCharts = $(".testCoverageChart");
        for (var i = 0; i < testCoverageCharts.length; i++) {
            createTestCoverageChart(testCoverageCharts[i]);
        }

        $(document).ready(function(){
            $("td > table").hide();
            var expandCollapseButtons = $(".cell-expand-collapse");

            if (expandCollapseButtons.length) {
                expandCollapseButtons.click(function(){
                    var elementToToggle = $(this).siblings("table");
                    elementToToggle.slideToggle("fast");
                    $(this).text($(this).text() == ' Expand' ? ' Collapse' : ' Expand');
                });
            }
        });
tools\PSCodeHealth\Assets\PSCodeHealthLogo.png
 
tools\PSCodeHealth\Private\Get-ExternalHelpCommand.ps1
Function Get-ExternalHelpCommand {
    <#
    .SYNOPSIS
        Gets the name of the commands listed in external help files.
    .DESCRIPTION
        Gets the name of the commands listed in external (MAML-formatted) help files.
    
    .PARAMETER Path
        Root directory where the function looks for external help files.  
        The function looks for files with a name ending with "-help.xml" in a "en-US" subdirectory.
    
    .EXAMPLE
        PS C:\> Get-ExternalHelpCommand -Path 'C:\GitRepos\MyModule'
    
        Gets the name of all the commands listed in external help files found in the folder : C:\GitRepos\MyModule\.
        
    .NOTES
        https://info.sapien.com/index.php/scripting/scripting-help/writing-xml-help-for-advanced-functions
    #>
    [CmdletBinding()]
    Param (
        [Parameter(Position=0, Mandatory)]
        [ValidateScript({ Test-Path $_ -PathType Container })]
        [string[]]$Path
    )

    $LocaleFolder = Get-ChildItem -Path $Path -Directory -Filter 'en-US' -Recurse
    If ( $LocaleFolder ) {
        $MamlHelpFile = Get-ChildItem -Path $LocaleFolder.FullName -File -Filter '*-help.xml'
        If ( $MamlHelpFile ) {
            Try {
                [xml]$Maml = Get-Content -Path $MamlHelpFile.FullName
            }
            Catch {
                Write-Warning "The content of the file $($MamlHelpFile.FullName) was not valid XML"
                return
            }
            return $Maml.helpItems.command.details.name
        }
    }
}
tools\PSCodeHealth\Private\Get-PowerShellFile.ps1
Function Get-PowerShellFile {
<#
.SYNOPSIS
    Gets all PowerShell files in the specified directory.
.DESCRIPTION
    Gets all PowerShell files (.ps1, .psm1 and .psd1) in the specified directory.
    The following PowerShell-related files are excluded : format data files, type data files and files containing Pester Tests.

.PARAMETER Path
    To specify the path of the directory to search.

.PARAMETER Recurse
    To search the Path directory and all subdirectories recursively.

.PARAMETER Exclude
    To specify file(s) to exclude. The value of this parameter qualifies the Path parameter.
    Enter a path element or pattern, such as *example*. Wildcards are permitted.

.EXAMPLE
    PS C:\> Get-PowerShellFile -Path C:\GitRepos\MyModule\ -Recurse

    Gets all PowerShell files in the directory C:\GitRepos\MyModule\ and any subdirectories.

.EXAMPLE
    PS C:\> Get-PowerShellFile -Path C:\GitRepos\MyModule\ -Recurse -Exclude "*example*"

    Gets PowerShell files in the directory C:\GitRepos\MyModule\ and any subdirectories, except for files containing "example" in their name.

.OUTPUTS
    System.String

#>
    [CmdletBinding()]
    [OutputType([String[]])]

    Param (
        [Parameter(Position=0, Mandatory, ValueFromPipeline=$True)]
        [ValidateScript({ Test-Path $_ -PathType Container })]
        [string]$Path,

        [switch]$Recurse,

        [Parameter(Mandatory=$False)]
        [string[]]$Exclude
    )

    $ChildItems = Get-ChildItem @PSBoundParameters -File
    $PowerShellFilter = { $_.Name -like '*.ps*1' }
    $PowerShellFiles = $ChildItems | Where-Object $PowerShellFilter

    Foreach ( $File in $PowerShellFiles ) {
        $FileAst = [System.Management.Automation.Language.Parser]::ParseFile($File.FullName, [ref]$Null, [ref]$Null)
        $Predicate = {
            Param($Ast) $Ast -is [System.Management.Automation.Language.CommandAst] -and
            $Ast.GetCommandName() -eq 'Describe' -and
            $Ast.CommandElements.StaticType -contains [scriptblock]
        }
        $DescribeBlock = $FileAst.Find($Predicate, $False)
        If ( -not($DescribeBlock) ) {
            $File.FullName
        }
    }
}
tools\PSCodeHealth\Private\HtmlReport\New-PSCodeHealthTableData.ps1
Function New-PSCodeHealthTableData {
<#
.SYNOPSIS
    Generate table rows for the HTML report, based on the data contained in a PSCodeHealth.Overall.HealthReport object.  

.DESCRIPTION
    Generate table rows for the HTML report, based on the data contained in a PSCodeHealth.Overall.HealthReport object.  
    This provides the rows for the following tables :  
      - Best Practices (per function)  
      - Maintainability (per function)  
      - Failed Tests Details  
      - Test Coverage (per function)  

.PARAMETER HealthReport
    To specify the input PSCodeHealth.Overall.HealthReport object containing the data.

.EXAMPLE
    New-PSCodeHealthTableData -HealthReport $HealthReport  

    This generates the rows for the tables Best Practices, Maintainability, Failed Tests Details and Test Coverage tables, based on the data in $HealthReport.  

.OUTPUTS
    PSCustomObject

.NOTES    
#>
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    Param (
        [Parameter(Mandatory, Position=0)]
        [PSTypeName('PSCodeHealth.Overall.HealthReport')]
        [PSCustomObject]$HealthReport
    )

    [System.Collections.ArrayList]$BestPracticesRows = @()
    Foreach ( $Function in $HealthReport.FunctionHealthRecords ) {

        If ( $Function.ScriptAnalyzerFindings -gt 0 ) {
            [System.Collections.ArrayList]$FindingsDetails = @()
            $Null = $FindingsDetails.Add(@"
`n                                            <button type="button" class="btn btn-{$($Function.FunctionName)_FINDINGS_DETAILS} btn-sm cell-expand-collapse"> Expand</button>
                                            <table>`n
"@)
            Foreach ( $Finding in $Function.ScriptAnalyzerResultDetails ) {

                $ScriptName = Split-Path -Path $Function.FilePath -Leaf
                $FindingDetail = @"
                                                <tr>
                                                    <td class="{$($Function.FunctionName)_FINDINGS_DETAILS} cell-largeContent">ScriptName : $ScriptName<br>
Line (in the function) : $($Finding.Line)<br>
Severity                      : $($Finding.Severity)<br>
RuleName                      : $($Finding.RuleName)<br>
Message                       : $($Finding.Message)<br>
                                                    </td>
                                                </tr>`n
"@
                $Null = $FindingsDetails.Add($FindingDetail)
            }
            $CloseTable = @"
                                            </table>`n                                        
"@
            $Null = $FindingsDetails.Add($CloseTable)
        }
        Else {
            [string]$FindingsDetails = ''
        }
        $Row = @"
                                    <tr>
                                        <td>$($Function.FunctionName)</td>
                                        <td class="{$($Function.FunctionName)_SCRIPTANALYZER_FINDINGS}">$($Function.ScriptAnalyzerFindings)</td>
                                        <td class="{$($Function.FunctionName)_FINDINGS_DETAILS}">$($FindingsDetails)</td>
                                        <td class="{$($Function.FunctionName)_CONTAINS_HELP}">$($Function.ContainsHelp)</td>
                                    </tr>
"@
        $Null = $BestPracticesRows.Add($Row)
    }

    [System.Collections.ArrayList]$MaintainabilityRows = @()
    Foreach ( $Function in $HealthReport.FunctionHealthRecords ) {

        $Row = @"
                                    <tr>
                                        <td>$($Function.FunctionName)</td>
                                        <td class="{$($Function.FunctionName)_LINES_OF_CODE_COMPLIANCE}">$($Function.LinesOfCode)</td>
                                        <td class="{$($Function.FunctionName)_COMPLEXITY_COMPLIANCE}">$($Function.Complexity)</td>
                                        <td class="{$($Function.FunctionName)_MAXIMUM_NESTING_DEPTH_COMPLIANCE}">$($Function.MaximumNestingDepth)</td>
                                    </tr>
"@
        $Null = $MaintainabilityRows.Add($Row)
    }

    If ( $HealthReport.NumberOfFailedTests -gt 0 ) {
        [System.Collections.ArrayList]$FailedTestsRows = @()
        Foreach ( $FailedTest in $HealthReport.FailedTestsDetails ) {
            
            $Row = @"
                                    <tr>
                                        <td>$($FailedTest.File)</td>
                                        <td>$($FailedTest.Line)</td>
                                        <td>$($FailedTest.Describe)</td>
                                        <td>$($FailedTest.TestName)</td>
                                        <td>$($FailedTest.ErrorMessage)</td>
                                    </tr>
"@
            $Null = $FailedTestsRows.Add($Row)
        }
    }
    Else {
        [string]$FailedTestsRows = ''
    }

    [System.Collections.ArrayList]$CoverageRows = @()
    Foreach ( $Function in $HealthReport.FunctionHealthRecords ) {

        $Row = @"
                                    <tr>
                                        <td>$($Function.FunctionName)</td>
                                        <td class="{$($Function.FunctionName)_TEST_COVERAGE_COMPLIANCE}">$($Function.TestCoverage)</td>
                                        <td class="{$($Function.FunctionName)_COMMANDS_MISSED_COMPLIANCE}">$($Function.CommandsMissed)</td>
                                    </tr>
"@
        $Null = $CoverageRows.Add($Row)
    }

    $ObjectProperties = [ordered]@{
        'BestPracticesRows'   = $BestPracticesRows
        'MaintainabilityRows' = $MaintainabilityRows
        'FailedTestsRows'     = $FailedTestsRows
        'CoverageRows'        = $CoverageRows
    }

    $CustomObject = New-Object -TypeName PSObject -Property $ObjectProperties
    return $CustomObject
}
tools\PSCodeHealth\Private\HtmlReport\Set-PSCodeHealthHtmlColor.ps1
Function Set-PSCodeHealthHtmlColor {
<#
.SYNOPSIS
    Sets classes to the elements in the HTML report which use color coding to reflect their compliance, and returns the modified HTML.  

.DESCRIPTION
    Sets the class attribute to the elements in the HTML report which use color coding to reflect their compliance.  
    These classes corresponds to CSS declaration blocks to apply the appropriate styling to the elements, in particular the colors.  
    Then, it returns the modified HTML content to the caller.

.PARAMETER HealthReport
    To specify the input PSCodeHealth.Overall.HealthReport object containing the data.

.PARAMETER Compliance
    To input the overall compliance information, based on the current health report and the compliance rules.

.PARAMETER PerFunctionCompliance
    To input the per-function compliance information, based on the functions in the current health report and the compliance rules.

.PARAMETER Html
    To input the original HTML content (containing placeholders to be substituted with the appropriate class values).

.EXAMPLE
    Set-PSCodeHealthHtmlColor -HealthReport $HealthReport -Compliance $OverallCompliance -PerFunctionCompliance $PerFunctionCompliance -Html $HtmlContent  

    This sets classes to the elements in the HTML report which use color coding to reflect their compliance and returns the modified HTML content.

.OUTPUTS
    System.String

.NOTES    
#>
    [CmdletBinding()]
    [OutputType([string[]])]
    Param (
        [Parameter(Mandatory, Position=0)]
        [PSTypeName('PSCodeHealth.Overall.HealthReport')]
        [PSCustomObject]$HealthReport,

        [Parameter(Mandatory, Position=1)]
        [PSTypeName('PSCodeHealth.Compliance.Result')]
        [PSCustomObject[]]$Compliance,

        [Parameter(Mandatory, Position=2)]
        [AllowNull()]
        [PSTypeName('PSCodeHealth.Compliance.FunctionResult')]
        [PSCustomObject[]]$PerFunctionCompliance,

        [Parameter(Mandatory, Position=2)]
        [AllowEmptyString()]
        [string[]]$Html
    )

        Function ConvertTo-HtmlClass ($ComplianceResult) {
            Switch ($ComplianceResult) {
                'Fail' { return 'danger' }
                'Warning' { return 'warning' }
                'Pass' { return 'success' }
                $True { return 'success' }
                $False { return 'danger' }
            }
        }

        Function Get-FindingsHtmlClass ([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]$Findings) {
            [string[]]$FindingsSeverity = $Findings.Severity
            If ( $FindingsSeverity -contains 'Error' ) {
                return 'danger'
            }
            If ( $FindingsSeverity -contains 'Warning' ) {
                return 'warning'
            }
            If ( $FindingsSeverity -contains 'Information' ) {
                return 'info'
            }
            return ''
        }

        $OverallPlaceholders = @{
            LINES_OF_CODE_TOTAL_COMPLIANCE = ConvertTo-HtmlClass $Compliance.Where({ $_.MetricName -eq 'LinesOfCodeTotal' }).Result
            SCRIPTANALYZER_TOTAL_COMPLIANCE = ConvertTo-HtmlClass $Compliance.Where({ $_.MetricName -eq 'ScriptAnalyzerFindingsTotal' }).Result
            SCRIPTANALYZER_ERRORS_COMPLIANCE = ConvertTo-HtmlClass $Compliance.Where({ $_.MetricName -eq 'ScriptAnalyzerErrors' }).Result
            SCRIPTANALYZER_WARNINGS_COMPLIANCE = ConvertTo-HtmlClass $Compliance.Where({ $_.MetricName -eq 'ScriptAnalyzerWarnings' }).Result
            SCRIPTANALYZER_INFO_COMPLIANCE = ConvertTo-HtmlClass $Compliance.Where({ $_.MetricName -eq 'ScriptAnalyzerInformation' }).Result
            TESTS_PASS_RATE_COMPLIANCE = ConvertTo-HtmlClass $Compliance.Where({ $_.MetricName -eq 'TestsPassRate' }).Result
            TEST_COVERAGE_COMPLIANCE = ConvertTo-HtmlClass $Compliance.Where({ $_.MetricName -eq 'TestCoverage' -and $_.SettingsGroup -eq 'OverallMetrics'}).Result
            SCRIPTANALYZER_AVERAGE_COMPLIANCE = ConvertTo-HtmlClass $Compliance.Where({ $_.MetricName -eq 'ScriptAnalyzerFindingsAverage' }).Result
            LINES_OF_CODE_AVERAGE_COMPLIANCE = ConvertTo-HtmlClass $Compliance.Where({ $_.MetricName -eq 'LinesOfCodeAverage' }).Result
            COMPLEXITY_AVERAGE_COMPLIANCE = ConvertTo-HtmlClass $Compliance.Where({ $_.MetricName -eq 'ComplexityAverage' }).Result
            NESTING_DEPTH_AVERAGE_COMPLIANCE = ConvertTo-HtmlClass $Compliance.Where({ $_.MetricName -eq 'NestingDepthAverage' }).Result
            NUMBER_OF_FAILED_TESTS_COMPLIANCE = ConvertTo-HtmlClass $Compliance.Where({ $_.MetricName -eq 'NumberOfFailedTests' }).Result
            COMMANDS_MISSED_TOTAL_COMPLIANCE = ConvertTo-HtmlClass $Compliance.Where({ $_.MetricName -eq 'CommandsMissedTotal' }).Result
            COMPLEXITY_HIGHEST_COMPLIANCE = ConvertTo-HtmlClass $Compliance.Where({ $_.MetricName -eq 'ComplexityHighest' }).Result
            NESTING_DEPTH_HIGHEST_COMPLIANCE = ConvertTo-HtmlClass $Compliance.Where({ $_.MetricName -eq 'NestingDepthHighest' }).Result
        }
        $HtmlOverall = Set-PSCodeHealthPlaceholdersValue -Html $Html -PlaceholdersData $OverallPlaceholders

        $HtmlFunction = $HtmlOverall
        Foreach ( $Function in $HealthReport.FunctionHealthRecords.FunctionName ) {

            $FunctionRecord = $HealthReport.FunctionHealthRecords.Where({ $_.FunctionName -eq $Function })

            $FunctionPlaceholders = @{
                "$($Function)_SCRIPTANALYZER_FINDINGS" = ConvertTo-HtmlClass $PerFunctionCompliance.Where({ $_.FunctionName -eq $Function -and $_.MetricName -eq 'ScriptAnalyzerFindings' }).Result
                "$($Function)_CONTAINS_HELP" = ConvertTo-HtmlClass $FunctionRecord.ContainsHelp
                "$($Function)_LINES_OF_CODE_COMPLIANCE" = ConvertTo-HtmlClass $PerFunctionCompliance.Where({ $_.FunctionName -eq $Function -and $_.MetricName -eq 'LinesOfCode' }).Result
                "$($Function)_COMPLEXITY_COMPLIANCE" = ConvertTo-HtmlClass $PerFunctionCompliance.Where({ $_.FunctionName -eq $Function -and $_.MetricName -eq 'Complexity' }).Result
                "$($Function)_MAXIMUM_NESTING_DEPTH_COMPLIANCE" = ConvertTo-HtmlClass $PerFunctionCompliance.Where({ $_.FunctionName -eq $Function -and $_.MetricName -eq 'MaximumNestingDepth' }).Result
                "$($Function)_TEST_COVERAGE_COMPLIANCE" = ConvertTo-HtmlClass $PerFunctionCompliance.Where({ $_.FunctionName -eq $Function -and $_.MetricName -eq 'TestCoverage' }).Result
                "$($Function)_COMMANDS_MISSED_COMPLIANCE" = ConvertTo-HtmlClass $PerFunctionCompliance.Where({ $_.FunctionName -eq $Function -and $_.MetricName -eq 'CommandsMissed' }).Result
                "$($Function)_FINDINGS_DETAILS" = Get-FindingsHtmlClass -Findings $FunctionRecord.ScriptAnalyzerResultDetails
            }
            $HtmlFunction = Set-PSCodeHealthPlaceholdersValue -Html $HtmlFunction -PlaceholdersData $FunctionPlaceholders
        }
        return $HtmlFunction
}
tools\PSCodeHealth\Private\HtmlReport\Set-PSCodeHealthPlaceholdersValue.ps1
Function Set-PSCodeHealthPlaceholdersValue {
<#
.SYNOPSIS
    Replaces Placeholders in template files with their specified value.  

.DESCRIPTION
    Replaces Placeholders in template files with their specified string value and outputs the new content with the replaced value.  

.PARAMETER TemplatePath
    Path of the template file containing placeholders to replace.  

.PARAMETER PlaceholdersData
    Hashtable with a key-value pair for each placeholder. The key is corresponds to the name of the placeholder to replace and the value corresponds to its string value.  

.EXAMPLE
    PS C:\> $PlaceholdersData = @{
        REPORT_TITLE = $HealthReport.ReportTitle
        ANALYZED_PATH = $HealthReport.AnalyzedPath
    }
    PS C:\> Set-PSCodeHealthPlaceholdersValue -TemplatePath '.\HealthReportTemplate.html' -PlaceholdersData $PlaceholdersData

    Returns the content of the template file with the placeholders 'REPORT_TITLE' and 'ANALYZED_PATH' substituted by the string values specified in the hashtable $PlaceholdersData.  

.OUTPUTS
    System.String

.NOTES    
#>
    [CmdletBinding(DefaultParameterSetName = 'File')]
    [OutputType([string[]])]
    Param (
        [Parameter(Position=0, Mandatory, ParameterSetName='File')]
        [ValidateScript({ Test-Path $_ -PathType Leaf })]
        [string]$TemplatePath,

        [Parameter(Position=1, Mandatory)]
        [Hashtable]$PlaceholdersData,

        [Parameter(Position=0, Mandatory, ParameterSetName='Html')]
        [AllowEmptyString()]
        [string[]]$Html
    )

    If ( $PSCmdlet.ParameterSetName -ne 'Html' ) {
        $Html = Get-Content -Path $TemplatePath
    }

    Foreach ( $Placeholder in $PlaceholdersData.GetEnumerator() ) {
        $PlaceholderPattern = '{{{0}}}' -f $Placeholder.Key

        # Handling values containing a collection
        $PlaceholderValue = If ( $($Placeholder.Value).Count -ne 1 ) { $Placeholder.Value | Out-String } Else { $Placeholder.Value }
        $Html = $Html.ForEach('Replace', $PlaceholderPattern, [string]$PlaceholderValue)
    }
    $Html
}
tools\PSCodeHealth\Private\Merge-PSCodeHealthSetting.ps1
Function Merge-PSCodeHealthSetting {
<#
.SYNOPSIS
    Merges user-defined settings (metrics thresholds, etc...) into the default PSCodeHealth settings.

.DESCRIPTION
    Merges user-defined settings (metrics thresholds, etc...) into the default PSCodeHealth settings.  
    The default PSCodeHealth settings are stored in PSCodeHealthSettings.json, but user-defined custom settings can override these defaults.  
    The custom settings are stored in JSON format in a file (similar to PSCodeHealthSettings.json).
    Any setting specified in the custom settings file override the default, and settings not specified in the custom settings file will use the defaults from PSCodeHealthSettings.json.  

.PARAMETER DefaultSettings
    PSCustomObject converted from the JSON data in PSCodeHealthSettings.json.

.PARAMETER CustomSettings
    PSCustomObject converted from the JSON data in a user-defined custom settings file.

.OUTPUTS
    System.Management.Automation.PSCustomObject
#>
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    Param(
        [Parameter(Mandatory,Position=0)]
        [PSCustomObject]$DefaultSettings,

        [Parameter(Mandatory,Position=1)]
        [PSCustomObject]$CustomSettings
    )

    # Checking if $CustomSettings contains something
    $ContainsSettings = $CustomSettings | Get-Member -MemberType Properties
    If ( -not($ContainsSettings) ) {
        Write-VerboseOutput -Message 'Custom settings do not contain any data, the resulting settings will be the defaults.'
        return $DefaultSettings
    }

    $ContainsFunctionHealthRecordSettings = 'PerFunctionMetrics' -in $ContainsSettings.Name
    $ContainsOverallHealthReportSettings = 'OverallMetrics' -in $ContainsSettings.Name

    If ( -not($ContainsFunctionHealthRecordSettings) -and -not($ContainsOverallHealthReportSettings) ) {
        Write-Warning -Message 'Custom settings do not contain any of the settings groups expected by PSCodeHealth.'
        return $DefaultSettings
    }

    If ( $ContainsFunctionHealthRecordSettings) {
        $CustomFunctionSettings = $CustomSettings.PerFunctionMetrics | Where-Object { $_ }

        # Casting to a list in case we need to add elements to it
        $DefaultFunctionSettings = ($DefaultSettings.PerFunctionMetrics | Where-Object { $_ }) -as [System.Collections.ArrayList]
                
        Foreach ( $CustomFunctionSetting in $CustomFunctionSettings ) {
            $MetricName = ($CustomFunctionSetting | Get-Member -MemberType Properties).Name
            Write-VerboseOutput -Message "Processing custom settings for metric : $MetricName"

            $DefaultFunctionSetting = $DefaultFunctionSettings | Where-Object { $_.$($MetricName) }
            If ( $DefaultFunctionSetting ) {
                Write-VerboseOutput -Message "The setting '$MetricName' is present in the default settings, overriding it."
                $DefaultFunctionSetting.$($MetricName) = $CustomFunctionSetting.$($MetricName)
            }
            Else {
                Write-VerboseOutput -Message "The setting '$MetricName' is absent from the default settings, adding it."
                $Null = $DefaultFunctionSettings.Add($CustomFunctionSetting)
            }
        }
    }

    If ( $ContainsOverallHealthReportSettings ) {
        $CustomOverallSettings = $CustomSettings.OverallMetrics | Where-Object { $_ }

        # Casting to a list in case we need to add elements to it
        $DefaultOverallSettings = ($DefaultSettings.OverallMetrics | Where-Object { $_ }) -as [System.Collections.ArrayList]

        Foreach ( $CustomOverallSetting in $CustomOverallSettings ) {
            $MetricName = ($CustomOverallSetting | Get-Member -MemberType Properties).Name
            Write-VerboseOutput -Message "Processing custom settings for metric : $MetricName"

            $DefaultOverallSetting = $DefaultOverallSettings | Where-Object { $_.$($MetricName) }
            If ( $DefaultOverallSetting ) {
                Write-VerboseOutput -Message "The setting '$MetricName' is present in the default settings, overriding it."
                $DefaultOverallSetting.$($MetricName) = $CustomOverallSetting.$($MetricName)
            }
            Else {
                Write-VerboseOutput -Message "The setting '$MetricName' is absent from the default settings, adding it."
                $Null = $DefaultOverallSettings.Add($CustomOverallSetting)
            }
        }
    }
    $MergedSettingsProperties = [ordered]@{
        PerFunctionMetrics = $DefaultFunctionSettings
        OverallMetrics = $DefaultOverallSettings
    }
    $MergedSettings = New-Object -TypeName PSCustomObject -Property $MergedSettingsProperties
    return $MergedSettings
}
tools\PSCodeHealth\Private\Metrics\Get-FunctionDefinition.ps1
Function Get-FunctionDefinition {
<#
.SYNOPSIS
    Gets all the function definitions in the specified files.
.DESCRIPTION
    Gets all the function definitions (including private functions but excluding nested functions) in the specified PowerShell file.

.PARAMETER Path
    To specify the path of the file to analyze.

.EXAMPLE
    PS C:\> Get-FunctionDefinition -Path C:\GitRepos\MyModule\MyModule.psd1

    Gets all function definitions in the module specified by its manifest, as FunctionDefinitionAst objects.

.OUTPUTS
    System.Management.Automation.Language.FunctionDefinitionAst

.NOTES
    
#>
    [CmdletBinding()]
    [OutputType([System.Management.Automation.Language.FunctionDefinitionAst[]])]
    Param (
        [Parameter(Position=0, Mandatory, ValueFromPipeline=$True)]
        [ValidateScript({ Test-Path $_ -PathType Leaf })]
        [string[]]$Path
    )
    Process {
        Foreach ( $PowerShellFile in $Path ) {
            Write-VerboseOutput -Message "Parsing file : $PowerShellFile"

            $PowerShellFile = (Resolve-Path -Path $PowerShellFile).Path
            $FileAst = [System.Management.Automation.Language.Parser]::ParseFile($PowerShellFile, [ref]$Null, [ref]$Null)
            
            $AstToInclude = [System.Management.Automation.Language.FunctionDefinitionAst]
            # Excluding class methods, since we don't support classes
            $AstToExclude = [System.Management.Automation.Language.FunctionMemberAst]

            $Predicate = { $args[0] -is $AstToInclude -and $args[0].Parent -isnot $AstToExclude }
            $FileFunctions = $FileAst.FindAll($Predicate, $False)
            If ( $FileFunctions ) {
                Foreach ( $FunctionName in $FileFunctions.Name ) {
                    Write-VerboseOutput -Message "Found function : $FunctionName"
                }
            }
            $FileFunctions
        }
    }
}
tools\PSCodeHealth\Private\Metrics\Get-FunctionLinesOfCode.ps1
Function Get-FunctionLinesOfCode {
<#
.SYNOPSIS
    Gets the number of lines in the specified function definition (excluding comments).
.DESCRIPTION
    Gets the number of lines of code in the specified function definition specified as a [System.Management.Automation.Language.FunctionDefinitionAst].
    The single line comments, multiple lines comments and comment-based help are not executable code, so they are excluded.

.PARAMETER FunctionDefinition
    To specify the function definition to analyze.

.EXAMPLE
    PS C:\> Get-FunctionLinesOfCode -FunctionDefinition $MyFunctionAst

    Returns the number of lines of code in the specified function definition.

.OUTPUTS
    System.Int32

.NOTES
    
#>
    [CmdletBinding()]
    [OutputType([System.Int32])]
    Param (
        [Parameter(Position=0, Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst]$FunctionDefinition
    )
    
    $FunctionText = $FunctionDefinition.Extent.Text
    Write-VerboseOutput -Message "Function name : $($FunctionDefinition.Name)"

    $AstTokens = [System.Management.Automation.PSParser]::Tokenize($FunctionText, [ref]$Null)
    $NoCommentTokens = $AstTokens.Where({ $_.Type -ne 'Comment' })

    # Substracting 1 from the number of lines if the last token is a NewLine
    [System.Int32]$NumberofLinesToSubstract = If ( $NoCommentTokens[-1].Type -eq 'NewLine' ) { 1 } Else { 0 }
    Write-VerboseOutput -Message "Number of lines to substract : $($NumberofLinesToSubstract)."

    [System.Int32]$NumberOfLines = ($NoCommentTokens.Where({ $_.Type -eq 'NewLine' })).Count - $NumberofLinesToSubstract
    return $NumberOfLines
}
tools\PSCodeHealth\Private\Metrics\Get-FunctionScriptAnalyzerResult.ps1
Function Get-FunctionScriptAnalyzerResult {
<#
.SYNOPSIS
    Gets the best practices violations details in the specified function definition, using PSScriptAnalyzer.
.DESCRIPTION
    Gets the best practices violations details in the specified function definition specified as a [System.Management.Automation.Language.FunctionDefinitionAst].
    It uses the PSScriptAnalyzer PowerShell module.

.PARAMETER FunctionDefinition
    To specify the function definition to analyze.

.EXAMPLE
    PS C:\> Get-FunctionScriptAnalyzerResult -FunctionDefinition $MyFunctionAst

    Returns the best practices violations details (PSScriptAnalyzer results) in the specified function definition.

.OUTPUTS
    Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord

.NOTES
    
#>
    [CmdletBinding()]
    [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])]
    Param (
        [Parameter(Position=0, Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst]$FunctionDefinition
    )
    
    $Results = Invoke-ScriptAnalyzer -ScriptDefinition $FunctionDefinition.Extent.Text -Verbose:$False
    return $Results
}
tools\PSCodeHealth\Private\Metrics\Get-FunctionTestCoverage.ps1
Function Get-FunctionTestCoverage {
<#
.SYNOPSIS
    Gets test coverage information for the specified function.

.DESCRIPTION
    Gets test coverage information for the specified function. This includes 2 pieces of information :  
      - Code coverage percentage (lines of code that are exercized by unit tests)  
      - Missed Commands (lines of codes or commands not being exercized by unit tests)  

    It uses Pester with its CodeCoverage parameter.

.PARAMETER FunctionDefinition
    To specify the function definition to analyze.

.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.
    If not specified, the directory of the file containing the specified function, and all subdirectories will be searched for tests.

.EXAMPLE
    PS C:\> Get-FunctionTestCoverage -FunctionDefinition $MyFunctionAst -TestsPath $MyModule.ModuleBase

    Gets test coverage information for the function $MyFunctionAst given the tests found in the module's parent directory.

.OUTPUTS
    PSCodeHealth.Function.TestCoverageInfo

.NOTES
    
#>
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    Param (
        [Parameter(Position=0, Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst]$FunctionDefinition,

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

    [string]$SourcePath = $FunctionDefinition.Extent.File
    $FunctionName = $FunctionDefinition.Name
    Write-VerboseOutput -Message "The function [$FunctionName] comes from the file :  $SourcePath"

    If ( -not $TestsPath ) {
        $TestsPath = Split-Path -Path $SourcePath -Parent
    }

    # Find all the files under the test path that contain the function name
    $Tests = Get-ChildItem -Path $TestsPath -Recurse -Filter *.tests.ps1 |
    Select-String -Pattern $FunctionName |
    Where-Object { $_.Line -notmatch 'Describe|Context|It |Mock ' } |
    Select-Object -ExpandProperty Path -Unique

    # 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 -Script $Tests -CodeCoverage @{ Path = $SourcePath; Function = $FunctionName } -PassThru -Verbose:$False @SuppressOutput

    If ( $TestsResult.CodeCoverage ) {
        $CodeCoverage = $TestsResult.CodeCoverage
        $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 exercized in the tests : $($CommandsExercised)"
            [System.Double]$CodeCoveragePerCent = [math]::Round(($CommandsExercised / $CommandsFound) * 100, 2)
        }
        Else {
            [System.Double]$CodeCoveragePerCent = 0
        }

        $ObjectProperties = [ordered]@{
            'CodeCoveragePerCent'         = $CodeCoveragePerCent
            'CommandsMissed'              = $CodeCoverage.MissedCommands
        }
        $CustomObject = New-Object -TypeName PSObject -Property $ObjectProperties
        $CustomObject.psobject.TypeNames.Insert(0, 'PSCodeHealth.Function.TestCoverageInfo')
        return $CustomObject
    }
}
tools\PSCodeHealth\Private\Metrics\Get-SwitchCombination.ps1
Function Get-SwitchCombination {
<#
.SYNOPSIS
    Calculates the number of additional code paths, given the number of Switch clauses which don't contain a Break statement.

.DESCRIPTION
    Calculates the number of additional code paths, given the number of Switch clauses which don't contain a Break statement.  
    The formula is 2 to the power of the input integer. This is based on :
    https://math.stackexchange.com/questions/161565/what-is-the-total-number-of-combinations-of-5-items-together-when-there-are-no-d
    But we don't substract 1, because the case where none of the Switch clause apply is a possible code path which should be included.

.PARAMETER Integer
    The number of clauses in a given Switch statement which don't contain a Break statement.

.EXAMPLE
    PS C:\> Get-SwitchCombination -Integer 3

    Calculates the number of additional code paths due to 3 clauses which don't contain a Break statement in a Switch statement.

.OUTPUTS
    System.Int32

.NOTES
    https://math.stackexchange.com/a/161568
#>
    [CmdletBinding()]
    [OutputType([System.Int32])]
    Param(
        [Parameter(Position=0, Mandatory)]
        [System.Int32]$Integer
    )

    $CombinationsTotal = If ( $Integer -le 1 ) { $Integer } Else { [System.Math]::Pow(2,$Integer) }

    If ( $CombinationsTotal -ge [System.Int32]::MaxValue ) {
        return [System.Int32]::MaxValue
    }
    return ($CombinationsTotal -as [System.Int32])
}
tools\PSCodeHealth\Private\Metrics\Measure-FunctionComplexity.ps1
Function Measure-FunctionComplexity {
<#
.SYNOPSIS
    Measures the code complexity.
.DESCRIPTION
    Measures the code complexity, in the specified function definition.
    This complexity is measured according to the Cyclomatic complexity.
    Cyclomatic complexity counts the number of possible paths through a given section of code.
    The number of possible paths depends on the number of conditional logic constructs, because conditional logic constructs are where the flow of execution branches out to one or more different path(s).

.PARAMETER FunctionDefinition
    To specify the function definition to analyze.

.EXAMPLE
    PS C:\> Measure-FunctionComplexity -FunctionDefinition $MyFunctionAst

    Gets the number of additional code paths due to While statements in the specified function definition.

.OUTPUTS
    System.Int32

.NOTES
    For more information on Cyclomatic complexity, please refer to the following article
    https://en.wikipedia.org/wiki/Cyclomatic_complexity

    A simple example of measuring the Cyclomatic complexity of a piece od code can be found here :
    https://www.tutorialspoint.com/software_testing_dictionary/cyclomatic_complexity.htm
#>
    [CmdletBinding()]
    [OutputType([System.Int32])]
    Param (
        [Parameter(Position=0, Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst]$FunctionDefinition
    )
    # Default complexity value for code which contains no branching statement (1 code path)
    [int]$DefaultComplexity = 1

    $ForPaths = Measure-FunctionForCodePath -FunctionDefinition $FunctionDefinition
    Write-VerboseOutput -Message "Number of code paths due to For loops : $($ForPaths)"

    $IfPaths = Measure-FunctionIfCodePath -FunctionDefinition $FunctionDefinition
    Write-VerboseOutput -Message "Number of code paths due to If and ElseIf statements : $($IfPaths)"

    $LogicalOpPaths = Measure-FunctionLogicalOpCodePath -FunctionDefinition $FunctionDefinition
    Write-VerboseOutput -Message "Number of code paths due to logical operators : $($LogicalOpPaths)"

    $SwitchPaths = Measure-FunctionSwitchCodePath -FunctionDefinition $FunctionDefinition
    Write-VerboseOutput -Message "Number of code paths due to Switch statements : $($SwitchPaths)"

    $TrapCatchPaths = Measure-FunctionTrapCatchCodePath -FunctionDefinition $FunctionDefinition
    Write-VerboseOutput -Message "Number of code paths due to Trap statements and Catch clauses : $($TrapCatchPaths)"

    $WhilePaths = Measure-FunctionWhileCodePath -FunctionDefinition $FunctionDefinition
    Write-VerboseOutput -Message "Number of code paths due to While loops : $($WhilePaths)"

    [int]$TotalComplexity = $DefaultComplexity + $ForPaths + $IfPaths + $LogicalOpPaths + $SwitchPaths + $TrapCatchPaths + $WhilePaths
    return $TotalComplexity
}
tools\PSCodeHealth\Private\Metrics\Measure-FunctionForCodePath.ps1
Function Measure-FunctionForCodePath {
<#
.SYNOPSIS
    Gets the number of additional code paths due to For loops.
.DESCRIPTION
    Gets the number of additional code paths due to For loops (where the For statement contains a condition), in the specified function definition.

.PARAMETER FunctionDefinition
    To specify the function definition to analyze.

.EXAMPLE
    PS C:\> Measure-FunctionForCodePath -FunctionDefinition $MyFunctionAst

    Gets the number of additional code paths due to for loops in the specified function definition.

.OUTPUTS
    System.Int32

.NOTES
    
#>
    [CmdletBinding()]
    [OutputType([System.Int32])]
    Param (
        [Parameter(Position=0, Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst]$FunctionDefinition
    )
    
    $FunctionText = $FunctionDefinition.Extent.Text

    # Converting the function definition to a generic ScriptBlockAst because the FindAll method of FunctionDefinitionAst object work strangely
    $FunctionAst = [System.Management.Automation.Language.Parser]::ParseInput($FunctionText, [ref]$null, [ref]$null)
    $ForStatements = $FunctionAst.FindAll({ $args[0] -is [System.Management.Automation.Language.ForStatementAst] }, $True)

    # Taking into account the rare cases where For statements don't contain a condition
    $ConditionalForStatements = $ForStatements | Where-Object Condition
    
    If ( -not($ConditionalForStatements) ) {
        return [int]0
    }
    return $ConditionalForStatements.Count
}
tools\PSCodeHealth\Private\Metrics\Measure-FunctionIfCodePath.ps1
Function Measure-FunctionIfCodePath {
<#
.SYNOPSIS
    Gets the number of additional code paths due to If statements.
.DESCRIPTION
    Gets the number of additional code paths due to If statements (including If/Else and If/ElseIf/Else statements), in the specified function definition.

.PARAMETER FunctionDefinition
    To specify the function definition to analyze.

.EXAMPLE
    PS C:\> Measure-FunctionIfCodePath -FunctionDefinition $MyFunctionAst

    Gets the number of additional code paths due to If statements in the specified function definition.

.OUTPUTS
    System.Int32

.NOTES
    
#>
    [CmdletBinding()]
    [OutputType([System.Int32])]
    Param (
        [Parameter(Position=0, Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst]$FunctionDefinition
    )
    
    $FunctionText = $FunctionDefinition.Extent.Text

    # Converting the function definition to a generic ScriptBlockAst because the FindAll method of FunctionDefinitionAst object work strangely
    $FunctionAst = [System.Management.Automation.Language.Parser]::ParseInput($FunctionText, [ref]$null, [ref]$null)
    $IfStatements = $FunctionAst.FindAll({ $args[0] -is [System.Management.Automation.Language.IfStatementAst] }, $True)

    If ( -not($IfStatements) ) {
        return [int]0
    }
    # If and ElseIf clauses are creating an additional path, not Else clauses
    return $IfStatements.Clauses.Count
}
tools\PSCodeHealth\Private\Metrics\Measure-FunctionLogicalOpCodePath.ps1
Function Measure-FunctionLogicalOpCodePath {
<#
.SYNOPSIS
    Gets the number of additional code paths due to Switch statements.
.DESCRIPTION
    Gets the number of additional code paths due to Switch statements, in the specified function definition.

.PARAMETER FunctionDefinition
    To specify the function definition to analyze.

.EXAMPLE
    PS C:\> Measure-FunctionLogicalOpCodePath -FunctionDefinition $MyFunctionAst

    Gets the number of additional code paths due to Switch statements in the specified function definition.

.OUTPUTS
    System.Int32

.NOTES
    
#>
    [CmdletBinding()]
    [OutputType([System.Int32])]
    Param (
        [Parameter(Position=0, Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst]$FunctionDefinition
    )
    
    $FunctionText = $FunctionDefinition.Extent.Text
    $Tokens = $Null
    $Null = [System.Management.Automation.Language.Parser]::ParseInput($FunctionText, [ref]$Tokens, [ref]$Null)
    $LogicalOperators = $Tokens.Where({$_.Kind.ToString() -In 'And','Or','Xor'})

    If ( -not($LogicalOperators) ) {
        return [int]0
    }
    return $LogicalOperators.Count
}
tools\PSCodeHealth\Private\Metrics\Measure-FunctionMaxNestingDepth.ps1
Function Measure-FunctionMaxNestingDepth {
<#
.SYNOPSIS
    Gets the depth of the most deeply nested statement in the function.
.DESCRIPTION
    Gets the depth of the most deeply nested statement in the function.
    Measuring the maximum nesting depth in a function is a way of evaluating its complexity.

.PARAMETER FunctionDefinition
    To specify the function definition to analyze.

.EXAMPLE
    PS C:\> Measure-FunctionMaxNestingDepth -FunctionDefinition $MyFunctionAst

    Gets the depth of the most deeply nested statement in the specified function definition.

.OUTPUTS
    System.Int32

.LINK
    Additional information on why maximum nesting depth is an interesting measure of code complexity :
    https://www.cqse.eu/en/blog/mccabe-cyclomatic-complexity/
    
#>
    [CmdletBinding()]
    [OutputType([System.Int32])]
    Param (
        [Parameter(Position=0, Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst]$FunctionDefinition
    )

    $FunctionText = $FunctionDefinition.Extent.Text
    $Tokens = $Null
    $Null = [System.Management.Automation.Language.Parser]::ParseInput($FunctionText, [ref]$Tokens, [ref]$Null)

    [System.Collections.ArrayList]$NestingDepthValues = @()
    [System.Int32]$NestingDepth = 0
    [System.Collections.ArrayList]$CurlyBraces = $Tokens.Where({ $_.Kind -in 'AtCurly','LCurly','RCurly' })

    # Removing the first opening curly and the last closing curly because they belong to the function itself
    $CurlyBraces.RemoveAt(0)
    $CurlyBraces.RemoveAt(($CurlyBraces.Count - 1))
    If ( -not $CurlyBraces ) {
        return $NestingDepth
    }

    Foreach ( $CurlyBrace in $CurlyBraces ) {

        If ( $CurlyBrace.Kind -in 'AtCurly','LCurly' ) {
            $NestingDepth++
        }
        ElseIf ( $CurlyBrace.Kind -eq 'RCurly' ) {
            $NestingDepth--
        }
        $Null = $NestingDepthValues.Add($NestingDepth)
    }
        Write-VerboseOutput -Message "Number of nesting depth values : $($NestingDepthValues.Count)"
        $MaxDepthValue = ($NestingDepthValues | Measure-Object -Maximum).Maximum -as [System.Int32]
        return $MaxDepthValue
}
tools\PSCodeHealth\Private\Metrics\Measure-FunctionSwitchCodePath.ps1
Function Measure-FunctionSwitchCodePath {
<#
.SYNOPSIS
    Gets the number of additional code paths due to Switch statements.
.DESCRIPTION
    Gets the number of additional code paths due to Switch statements, in the specified function definition.

.PARAMETER FunctionDefinition
    To specify the function definition to analyze.

.EXAMPLE
    PS C:\> Measure-FunctionSwitchCodePath -FunctionDefinition $MyFunctionAst

    Gets the number of additional code paths due to Switch statements in the specified function definition.

.OUTPUTS
    System.Int32

.NOTES
    
#>
    [CmdletBinding()]
    [OutputType([System.Int32])]
    Param (
        [Parameter(Position=0, Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst]$FunctionDefinition
    )
    
    $FunctionText = $FunctionDefinition.Extent.Text

    # Converting the function definition to a generic ScriptBlockAst because the FindAll method of FunctionDefinitionAst object work strangely
    $FunctionAst = [System.Management.Automation.Language.Parser]::ParseInput($FunctionText, [ref]$null, [ref]$null)
    $SwitchStatements = $FunctionAst.FindAll({ $args[0] -is [System.Management.Automation.Language.SwitchStatementAst] }, $True)

    If ( -not($SwitchStatements) ) {
        return [int]0
    }
    [int]$SwitchCodePaths = 0
    Foreach ( $SwitchStatement in $SwitchStatements ) {
        [int]$ClausesWithBreak = (@($SwitchStatement.Clauses).Where({ $_ -match 'Break' })).Count
        [int]$ClausesWithoutBreak = (@($SwitchStatement.Clauses).Where({ $_ -notmatch 'Break' })).Count
        $SwitchCodePaths += ($ClausesWithBreak + (Get-SwitchCombination -Integer $ClausesWithoutBreak))
    }
    # Each clause is creating an additional path, except for the "catch-all" Default clause
    return $SwitchCodePaths
}
tools\PSCodeHealth\Private\Metrics\Measure-FunctionTrapCatchCodePath.ps1
Function Measure-FunctionTrapCatchCodePath {
<#
.SYNOPSIS
    Gets the number of additional code paths due to Trap statements and Catch clauses in Try statements.
.DESCRIPTION
    Gets the number of additional code paths due to Trap statements and Catch clauses in Try statements, in the specified function definition.

.PARAMETER FunctionDefinition
    To specify the function definition to analyze.

.EXAMPLE
    PS C:\> Measure-FunctionTrapCatchCodePath -FunctionDefinition $MyFunctionAst

    Gets the number of additional code paths due to Trap statements and Catch clauses in Try statements, in the specified function definition.

.OUTPUTS
    System.Int32

.NOTES
    
#>
    [CmdletBinding()]
    [OutputType([System.Int32])]
    Param (
        [Parameter(Position=0, Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst]$FunctionDefinition
    )
    
    $FunctionText = $FunctionDefinition.Extent.Text

    # Converting the function definition to a generic ScriptBlockAst because the FindAll method of FunctionDefinitionAst object work strangely
    $FunctionAst = [System.Management.Automation.Language.Parser]::ParseInput($FunctionText, [ref]$null, [ref]$null)
    $TrapStatements = $FunctionAst.FindAll({ $args[0] -is [System.Management.Automation.Language.TrapStatementAst] }, $True)
    $CatchClauses = $FunctionAst.FindAll({ $args[0] -is [System.Management.Automation.Language.CatchClauseAst] }, $True)

    [int]$ErrorHandlingCodePaths = $TrapStatements.Count + $CatchClauses.Count
    return $ErrorHandlingCodePaths
}
tools\PSCodeHealth\Private\Metrics\Measure-FunctionWhileCodePath.ps1
Function Measure-FunctionWhileCodePath {
<#
.SYNOPSIS
    Gets the number of additional code paths due to While statements.
.DESCRIPTION
    Gets the number of additional code paths due to While statements, in the specified function definition.

.PARAMETER FunctionDefinition
    To specify the function definition to analyze.

.EXAMPLE
    PS C:\> Measure-FunctionWhileCodePath -FunctionDefinition $MyFunctionAst

    Gets the number of additional code paths due to While statements in the specified function definition.

.OUTPUTS
    System.Int32

.NOTES
    
#>
    [CmdletBinding()]
    [OutputType([System.Int32])]
    Param (
        [Parameter(Position=0, Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst]$FunctionDefinition
    )
    
    $FunctionText = $FunctionDefinition.Extent.Text

    # Converting the function definition to a generic ScriptBlockAst because the FindAll method of FunctionDefinitionAst object work strangely
    $FunctionAst = [System.Management.Automation.Language.Parser]::ParseInput($FunctionText, [ref]$null, [ref]$null)
    $WhileStatements = $FunctionAst.FindAll({ $args[0] -is [System.Management.Automation.Language.WhileStatementAst] }, $True)

    If ( -not($WhileStatements) ) {
        return [int]0
    }
    return $WhileStatements.Count
}
tools\PSCodeHealth\Private\Metrics\Test-FunctionHelpCoverage.ps1
Function Test-FunctionHelpCoverage {
<#
.SYNOPSIS
    Tells whether or not the specified function definition contains help information.
.DESCRIPTION
    Tells whether or not the specified function definition specified as a [System.Management.Automation.Language.FunctionDefinitionAst] contains help information.
    This function returns $True if the specified function definition AST has a CommentHelpInfo or if the function name is listed in an external help file.

.PARAMETER FunctionDefinition
    To specify the function definition to analyze.

.EXAMPLE
    PS C:\> Test-FunctionHelpCoverage -FunctionDefinition $MyFunctionAst

    Returns $True if the specified function definition contains help information, returns $False if not.

.OUTPUTS
    System.Boolean

.NOTES
    
#>
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    Param (
        [Parameter(Position=0, Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst]$FunctionDefinition
    )
    
    $FunctionHelpInfo = $FunctionDefinition.GetHelpContent()
    [bool]$CommentBasedHelpPresent = $FunctionHelpInfo -is [System.Management.Automation.Language.CommentHelpInfo]
    
    [bool]$ExternalHelpPresent = $FunctionDefinition.Name -in $Script:ExternalHelpCommandNames
    $HelpPresent = $CommentBasedHelpPresent -or $ExternalHelpPresent
    return $HelpPresent
}
tools\PSCodeHealth\Private\New-FailedTestsInfo.ps1
Function New-FailedTestsInfo {
<#
.SYNOPSIS
    Creates one or more custom objects of the type : 'PSCodeHealth.Overall.FailedTestsInfo'.  

.DESCRIPTION
    Creates one or more custom objects of the type : 'PSCodeHealth.Overall.FailedTestsInfo'.  
    This outputs an object containing key information about each failed test. This information is used in the overall health report.  

.PARAMETER TestsResult
    To specify the Pester tests result object.

.EXAMPLE
    PS C:\> New-FailedTestsInfo -TestsResult $TestsResult

    Returns a new custom object of the type 'PSCodeHealth.Overall.FailedTestsInfo' for each failed test in the input $TestsResult.

.OUTPUTS
    PSCodeHealth.Overall.FailedTestsInfo

.NOTES    
#>
    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    Param (
        [Parameter(Position=0, Mandatory)]
        [PSCustomObject]$TestsResult
    )
    $FailedTests = $TestsResult.TestResult.Where({ -not $_.Passed })

    Foreach ( $FailedTest in $FailedTests ) {

        $SplitStackTrace = $FailedTest.StackTrace -split ':\s'
        $File = ($SplitStackTrace[0] -split '\\')[-1]
        $Line = ((($SplitStackTrace[1] -split '\n') | Where-Object { $_ -match 'line' }) -split '\s')
        $LineNumber = $Line | Where-Object { $_ -match '\d+' }

        $ObjectProperties = [ordered]@{
            'File'         = $File
            'Line'         = $LineNumber
            'Describe'     = $FailedTest.Describe
            'TestName'     = $FailedTest.Name
            'ErrorMessage' = $FailedTest.FailureMessage
        }

        $CustomObject = New-Object -TypeName PSObject -Property $ObjectProperties
        $CustomObject.psobject.TypeNames.Insert(0, 'PSCodeHealth.Overall.FailedTestsInfo')
        $CustomObject
    }
}
tools\PSCodeHealth\Private\New-FunctionHealthRecord.ps1
Function New-FunctionHealthRecord {
<#
.SYNOPSIS
    Creates a new custom object and gives it the TypeName : 'PSCodeHealth.Function.HealthRecord'.
.DESCRIPTION
    Creates a new custom object and gives it the TypeName : 'PSCodeHealth.Function.HealthRecord'.

.PARAMETER FunctionDefinition
    To specify the function definition.

.PARAMETER FunctionTestCoverage
    To specify the percentage of lines of code in the specified function that are tested by unit tests.

.EXAMPLE
    PS C:\> New-FunctionHealthRecord -FunctionDefinition $MyFunctionAst -FunctionTestCoverage $TestCoverage

    Returns new custom object of the type PSCodeHealth.Function.HealthRecord.

.OUTPUTS
    PSCodeHealth.Function.HealthRecord

.NOTES
    
#>
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    Param (
        [Parameter(Position=0, Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst]$FunctionDefinition,

        [Parameter(Position=1, Mandatory)]
        [AllowNull()]
        [PSTypeName('PSCodeHealth.Function.TestCoverageInfo')]
        [PSCustomObject]$FunctionTestCoverage
    )

    $ScriptAnalyzerResultDetails = Get-FunctionScriptAnalyzerResult -FunctionDefinition $FunctionDefinition

    $ObjectProperties = [ordered]@{
        'FunctionName'                = $FunctionDefinition.Name
        'FilePath'                    = $FunctionDefinition.Extent.File
        'LinesOfCode'                 = Get-FunctionLinesOfCode -FunctionDefinition $FunctionDefinition
        'ScriptAnalyzerFindings'      = $ScriptAnalyzerResultDetails.Count
        'ScriptAnalyzerResultDetails' = $ScriptAnalyzerResultDetails
        'ContainsHelp'                = Test-FunctionHelpCoverage -FunctionDefinition $FunctionDefinition
        'TestCoverage'                = $FunctionTestCoverage.CodeCoveragePerCent
        'CommandsMissed'              = ($FunctionTestCoverage.CommandsMissed | Measure-Object).Count
        'Complexity'                  = Measure-FunctionComplexity -FunctionDefinition $FunctionDefinition
        'MaximumNestingDepth'         = Measure-FunctionMaxNestingDepth -FunctionDefinition $FunctionDefinition
    }

    $CustomObject = New-Object -TypeName PSObject -Property $ObjectProperties
    $CustomObject.psobject.TypeNames.Insert(0, 'PSCodeHealth.Function.HealthRecord')
    return $CustomObject
}
tools\PSCodeHealth\Private\New-PSCodeHealthComplianceResult.ps1
Function New-PSCodeHealthComplianceResult {
<#
.SYNOPSIS
    Creates a new custom object and gives it the TypeName : 'PSCodeHealth.Compliance.Result'.  

.DESCRIPTION
    Creates a new custom object based on a PSCodeHealth.Compliance.Rule object and a compliance result, and gives it the TypeName : 'PSCodeHealth.Compliance.Result'.  

.PARAMETER ComplianceRule
    The compliance rule which was evaluated.

.PARAMETER Value
    The value from the health report for the evaluated metric.

.PARAMETER Result
    The compliance result, based on the compliance rule and the actual value from the health report.

.PARAMETER FunctionName
    To get compliance results for a specific function.  
    If this parameter is specified, this creates a PSCodeHealth.Compliance.FunctionResult object, instead of PSCodeHealth.Compliance.Result.

.EXAMPLE
    PS C:\> New-PSCodeHealthComplianceResult -ComplianceRule $Rule -Value 81.26 -Result Warning

    Returns new custom object of the type PSCodeHealth.Compliance.Result.

.EXAMPLE
    PS C:\> New-PSCodeHealthComplianceResult -ComplianceRule $Rule -Value 81.26 -Result Warning -FunctionName 'Get-Something'

    Returns new custom object of the type PSCodeHealth.Compliance.FunctionResult for the function 'Get-Something'.

.OUTPUTS
    PSCodeHealth.Compliance.Result, PSCodeHealth.Compliance.FunctionResult
#>
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    Param (
        [Parameter(Mandatory, Position=0)]
        [PSTypeName('PSCodeHealth.Compliance.Rule')]
        [PSCustomObject]$ComplianceRule,

        [Parameter(Mandatory, Position=1)]
        [PSObject]$Value,

        [Parameter(Mandatory, Position=2)]
        [ValidateSet('Fail','Warning','Pass')]
        [string]$Result,

        [Parameter(Mandatory=$False, Position=3)]
        [string]$FunctionName
    )

    $PropsDictionary = [ordered]@{
        'SettingsGroup'    = $ComplianceRule.SettingsGroup
        'MetricName'       = $ComplianceRule.MetricName
        'WarningThreshold' = $ComplianceRule.WarningThreshold
        'FailThreshold'    = $ComplianceRule.FailThreshold
        'HigherIsBetter'   = $ComplianceRule.HigherIsBetter
        'Value'            = $Value
        'Result'           = $Result
    }

    If ( $PSBoundParameters.ContainsKey('FunctionName') ) {
        $PropsDictionary.Insert(0, 'FunctionName', $FunctionName)

        $CustomObject = New-Object -TypeName PSObject -Property $PropsDictionary
        $CustomObject.psobject.TypeNames.Insert(0, 'PSCodeHealth.Compliance.FunctionResult')
    }
    Else {
        $CustomObject = New-Object -TypeName PSObject -Property $PropsDictionary
        $CustomObject.psobject.TypeNames.Insert(0, 'PSCodeHealth.Compliance.Result')
    }
    return $CustomObject
}
tools\PSCodeHealth\Private\New-PSCodeHealthComplianceRule.ps1
Function New-PSCodeHealthComplianceRule {
<#
.SYNOPSIS
    Creates a new custom object and gives it the TypeName : 'PSCodeHealth.Compliance.Rule'.
.DESCRIPTION
    Creates a new custom object and gives it the TypeName : 'PSCodeHealth.Compliance.Rule'.

.PARAMETER MetricRule
    To specify the original metric rule object.

.PARAMETER SettingsGroup
    To specify from which settings group the current metric rule comes from.

.EXAMPLE
    PS C:\> New-PSCodeHealthComplianceRule -MetricRule $MetricRule -SettingsGroup PerFunctionMetrics

    Returns new custom object of the type PSCodeHealth.Compliance.Rule.

.OUTPUTS
    PSCodeHealth.Compliance.Rule
#>
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    Param (
        [Parameter(Mandatory, Position=0)]
        [PSCustomObject]$MetricRule,

        [Parameter(Mandatory, Position=1)]
        [ValidateSet('PerFunctionMetrics','OverallMetrics')]
        [string]$SettingsGroup
    )

    $MetricName = ($MetricRule | Get-Member -MemberType Properties).Name

    $ObjectProperties = [ordered]@{
        'SettingsGroup'    = $SettingsGroup
        'MetricName'       = $MetricName
        'WarningThreshold' = $MetricRule.$($MetricName).WarningThreshold
        'FailThreshold'    = $MetricRule.$($MetricName).FailThreshold
        'HigherIsBetter'   = $MetricRule.$($MetricName).HigherIsBetter
    }

    $CustomObject = New-Object -TypeName PSObject -Property $ObjectProperties
    $CustomObject.psobject.TypeNames.Insert(0, 'PSCodeHealth.Compliance.Rule')
    return $CustomObject
}
tools\PSCodeHealth\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
}
tools\PSCodeHealth\Private\Write-VerboseOutput.ps1
Function Write-VerboseOutput {
<#
.SYNOPSIS
    Helper function for semi-structured logging, to manage verbose output of the other functions.
#>
    [CmdletBinding()]
    Param(
        [Parameter(Position=0, Mandatory)]
        [string]$Message
    )

    $TimeStamp = Get-Date -Format s
    $CallingCommand = (Get-PSCallStack)[1].Command
    $MessageData = '{0} [{1}] : {2}' -f $TimeStamp,$CallingCommand,$Message

    Write-Verbose -Message $MessageData
}
tools\PSCodeHealth\PSCodeHealth.Format.ps1xml
 
tools\PSCodeHealth\PSCodeHealth.psd1
#
# Module manifest for module 'PSCodeHealth'
#
# Generated by: Mathieu Buisson
#
# Generated on: 05/02/2017
#

@{

# Script module or binary module file associated with this manifest.
RootModule = '.\PSCodeHealth.psm1'

# Version number of this module.
ModuleVersion = '0.2.26'

# ID used to uniquely identify this module
GUID = 'ca22dabd-bbb6-4805-9c90-a8aad6dbbfd3'

# Author of this module
Author = 'Mathieu Buisson'

# Company or vendor of this module
CompanyName = 'Unknown'

# Copyright statement for this module
Copyright = '(c) 2017 Mathieu Buisson. All rights reserved.'

# Description of the functionality provided by this module
Description = 'This module allows you to measure the quality and maintainability of your PowerShell code, based on a variety of metrics.'

# Minimum version of the Windows PowerShell engine required by this module
PowerShellVersion = '5.0'

# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''

# Minimum version of the Windows PowerShell host required by this module
# PowerShellHostVersion = ''

# Minimum version of Microsoft .NET Framework required by this module
# DotNetFrameworkVersion = ''

# Minimum version of the common language runtime (CLR) required by this module
# CLRVersion = ''

# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''

# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @('Pester','PSScriptAnalyzer')

# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()

# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()

# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()

# Format files (.ps1xml) to be loaded when importing this module
FormatsToProcess = @('PSCodeHealth.Format.ps1xml')

# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
# NestedModules = @()

# Functions to export from this module
FunctionsToExport = @('Invoke-PSCodeHealth','Get-PSCodeHealthComplianceRule','Test-PSCodeHealthCompliance')

# Cmdlets to export from this module
# CmdletsToExport = '*'

# Variables to export from this module
# VariablesToExport = '*'

# Aliases to export from this module
AliasesToExport = 'ipch'

# DSC resources to export from this module
# DscResourcesToExport = @()

# List of all modules packaged with this module
# ModuleList = @()

# List of all files packaged with this module
# FileList = @()

# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{

    PSData = @{

        # Tags applied to this module. These help with module discovery in online galleries.
        Tags = 'PowerShell', 'Quality', 'Metrics', 'DevOps'

        # A URL to the license for this module.
        LicenseUri = 'https://github.com/MathieuBuisson/PSCodeHealth/blob/master/LICENSE.md'

        # A URL to the main website for this project.
        ProjectUri = 'https://github.com/MathieuBuisson/PSCodeHealth'

        # A URL to an icon representing this module.
        IconUri = 'https://github.com/MathieuBuisson/PSCodeHealth/raw/master/PSCodeHealth/Assets/PSCodeHealthLogo.png'

        # ReleaseNotes of this module
        ReleaseNotes = 'https://github.com/MathieuBuisson/PSCodeHealth/blob/master/docs/Release.md'

    } # End of PSData hashtable

} # End of PrivateData hashtable

# HelpInfo URI of this module
# HelpInfoURI = ''

# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
# DefaultCommandPrefix = ''

}
tools\PSCodeHealth\PSCodeHealth.psm1
#Get public and private function definition files.
$Public  = @( Get-ChildItem -Path "$PSScriptRoot\Public\*.ps1" -File -ErrorAction SilentlyContinue )
$Private = @( Get-ChildItem -Path "$PSScriptRoot\Private" -File -Filter '*.ps1' -Recurse -ErrorAction SilentlyContinue )

Foreach ( $Import in @($Public + $Private) ) {
    Try {
        . $Import.FullName
    }
    Catch {
        Write-Error -Message "Failed to import function $($Import.FullName): $_"
    }
}
$Script:ExternalHelpCommandNames = @()

Export-ModuleMember -Function $Public.Basename
Set-Alias -Name ipch -Value Invoke-PSCodeHealth -Force
Export-ModuleMember -Alias 'ipch'
tools\PSCodeHealth\PSCodeHealthSettings.json
{
    "PerFunctionMetrics": [
        {
            "LinesOfCode": {
                "WarningThreshold": 30,
                "FailThreshold": 60,
                "HigherIsBetter": false
            }
        },
        {
            "ScriptAnalyzerFindings": {
                "WarningThreshold": 7,
                "FailThreshold": 12,
                "HigherIsBetter": false
            }
        },
        {
            "TestCoverage": {
                "WarningThreshold": 80,
                "FailThreshold": 70,
                "HigherIsBetter": true
            }
        },
        {
            "CommandsMissed": {
                "WarningThreshold": 6,
                "FailThreshold": 12,
                "HigherIsBetter": false
            }
        },        
        {
            "Complexity": {
                "WarningThreshold": 15,
                "FailThreshold": 30,
                "HigherIsBetter": false
            }
        },
        {
            "MaximumNestingDepth": {
                "WarningThreshold": 4,
                "FailThreshold": 8,
                "HigherIsBetter": false
            }
        }
    ],
    "OverallMetrics": [
        {
            "LinesOfCodeTotal": {
                "WarningThreshold": 1000,
                "FailThreshold": 2000,
                "HigherIsBetter": false
            }
        },
        {
            "LinesOfCodeAverage": {
                "WarningThreshold": 30,
                "FailThreshold": 60,
                "HigherIsBetter": false
            }
        },
        {
            "ScriptAnalyzerFindingsTotal": {
                "WarningThreshold": 30,
                "FailThreshold": 60,
                "HigherIsBetter": false
            }
        },
        {
            "ScriptAnalyzerErrors": {
                "WarningThreshold": 1,
                "FailThreshold": 3,
                "HigherIsBetter": false
            }
        },
        {
            "ScriptAnalyzerWarnings": {
                "WarningThreshold": 10,
                "FailThreshold": 20,
                "HigherIsBetter": false
            }
        },
        {
            "ScriptAnalyzerInformation": {
                "WarningThreshold": 20,
                "FailThreshold": 40,
                "HigherIsBetter": false
            }
        },
        {
            "ScriptAnalyzerFindingsAverage": {
                "WarningThreshold": 7,
                "FailThreshold": 12,
                "HigherIsBetter": false
            }
        },
        {
            "NumberOfFailedTests": {
                "WarningThreshold": 1,
                "FailThreshold": 3,
                "HigherIsBetter": false
            }
        },
        {
            "TestsPassRate": {
                "WarningThreshold": 99,
                "FailThreshold": 97,
                "HigherIsBetter": true
            }
        },
        {
            "TestCoverage": {
                "WarningThreshold": 80,
                "FailThreshold": 70,
                "HigherIsBetter": true
            }
        },
        {
            "CommandsMissedTotal": {
                "WarningThreshold": 200,
                "FailThreshold": 400,
                "HigherIsBetter": false
            }
        },
        {
            "ComplexityAverage": {
                "WarningThreshold": 15,
                "FailThreshold": 30,
                "HigherIsBetter": false
            }
        },
        {
            "ComplexityHighest": {
                "WarningThreshold": 30,
                "FailThreshold": 60,
                "HigherIsBetter": false
            }
        },
        {
            "NestingDepthAverage": {
                "WarningThreshold": 4,
                "FailThreshold": 8,
                "HigherIsBetter": false
            }
        },
        {
            "NestingDepthHighest": {
                "WarningThreshold": 8,
                "FailThreshold": 16,
                "HigherIsBetter": false
            }
        }
    ]
}
tools\PSCodeHealth\PSGetModuleInfo.xml
<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
  <Obj RefId="0">
    <TN RefId="0">
      <T>Microsoft.PowerShell.Commands.PSRepositoryItemInfo</T>
      <T>System.Management.Automation.PSCustomObject</T>
      <T>System.Object</T>
    </TN>
    <MS>
      <S N="Name">PSCodeHealth</S>
      <Version N="Version">0.2.26</Version>
      <S N="Type">Module</S>
      <S N="Description">This module allows you to measure the quality and maintainability of your PowerShell code, based on a variety of metrics.</S>
      <S N="Author">Mathieu Buisson</S>
      <S N="CompanyName">MathieuBuisson</S>
      <S N="Copyright">(c) 2017 Mathieu Buisson. All rights reserved.</S>
      <DT N="PublishedDate">2018-05-10T16:50:31+00:00</DT>
      <Nil N="InstalledDate" />
      <Nil N="UpdatedDate" />
      <URI N="LicenseUri">https://github.com/MathieuBuisson/PSCodeHealth/blob/master/LICENSE.md</URI>
      <URI N="ProjectUri">https://github.com/MathieuBuisson/PSCodeHealth</URI>
      <URI N="IconUri">https://github.com/MathieuBuisson/PSCodeHealth/raw/master/PSCodeHealth/Assets/PSCodeHealthLogo.png</URI>
      <Obj N="Tags" RefId="1">
        <TN RefId="1">
          <T>System.Object[]</T>
          <T>System.Array</T>
          <T>System.Object</T>
        </TN>
        <LST>
          <S>PowerShell</S>
          <S>Quality</S>
          <S>Metrics</S>
          <S>DevOps</S>
          <S>PSModule</S>
        </LST>
      </Obj>
      <Obj N="Includes" RefId="2">
        <TN RefId="2">
          <T>System.Collections.Hashtable</T>
          <T>System.Object</T>
        </TN>
        <DCT>
          <En>
            <S N="Key">Function</S>
            <Obj N="Value" RefId="3">
              <TNRef RefId="1" />
              <LST>
                <S>Invoke-PSCodeHealth</S>
                <S>Get-PSCodeHealthComplianceRule</S>
                <S>Test-PSCodeHealthCompliance</S>
              </LST>
            </Obj>
          </En>
          <En>
            <S N="Key">RoleCapability</S>
            <Obj N="Value" RefId="4">
              <TNRef RefId="1" />
              <LST />
            </Obj>
          </En>
          <En>
            <S N="Key">Command</S>
            <Obj N="Value" RefId="5">
              <TNRef RefId="1" />
              <LST>
                <S>Invoke-PSCodeHealth</S>
                <S>Get-PSCodeHealthComplianceRule</S>
                <S>Test-PSCodeHealthCompliance</S>
              </LST>
            </Obj>
          </En>
          <En>
            <S N="Key">DscResource</S>
            <Obj N="Value" RefId="6">
              <TNRef RefId="1" />
              <LST />
            </Obj>
          </En>
          <En>
            <S N="Key">Workflow</S>
            <Obj N="Value" RefId="7">
              <TNRef RefId="1" />
              <LST />
            </Obj>
          </En>
          <En>
            <S N="Key">Cmdlet</S>
            <Obj N="Value" RefId="8">
              <TNRef RefId="1" />
              <LST />
            </Obj>
          </En>
        </DCT>
      </Obj>
      <Nil N="PowerShellGetFormatVersion" />
      <S N="ReleaseNotes">https://github.com/MathieuBuisson/PSCodeHealth/blob/master/docs/Release.md</S>
      <Obj N="Dependencies" RefId="9">
        <TNRef RefId="1" />
        <LST>
          <Obj RefId="10">
            <TN RefId="3">
              <T>System.Collections.Specialized.OrderedDictionary</T>
              <T>System.Object</T>
            </TN>
            <DCT>
              <En>
                <S N="Key">Name</S>
                <S N="Value">Pester</S>
              </En>
              <En>
                <S N="Key">CanonicalId</S>
                <S N="Value">nuget:Pester</S>
              </En>
            </DCT>
          </Obj>
          <Obj RefId="11">
            <TNRef RefId="3" />
            <DCT>
              <En>
                <S N="Key">Name</S>
                <S N="Value">PSScriptAnalyzer</S>
              </En>
              <En>
                <S N="Key">CanonicalId</S>
                <S N="Value">nuget:PSScriptAnalyzer</S>
              </En>
            </DCT>
          </Obj>
        </LST>
      </Obj>
      <S N="RepositorySourceLocation">https://www.powershellgallery.com/api/v2/</S>
      <S N="Repository">PSGallery</S>
      <S N="PackageManagementProvider">NuGet</S>
      <Obj N="AdditionalMetadata" RefId="12">
        <TNRef RefId="2" />
        <DCT>
          <En>
            <S N="Key">releaseNotes</S>
            <S N="Value">https://github.com/MathieuBuisson/PSCodeHealth/blob/master/docs/Release.md</S>
          </En>
          <En>
            <S N="Key">versionDownloadCount</S>
            <S N="Value">11</S>
          </En>
          <En>
            <S N="Key">ItemType</S>
            <S N="Value">Module</S>
          </En>
          <En>
            <S N="Key">copyright</S>
            <S N="Value">(c) 2017 Mathieu Buisson. All rights reserved.</S>
          </En>
          <En>
            <S N="Key">CompanyName</S>
            <S N="Value">Unknown</S>
          </En>
          <En>
            <S N="Key">tags</S>
            <S N="Value">PowerShell Quality Metrics DevOps PSModule PSFunction_Invoke-PSCodeHealth PSCommand_Invoke-PSCodeHealth PSFunction_Get-PSCodeHealthComplianceRule PSCommand_Get-PSCodeHealthComplianceRule PSFunction_Test-PSCodeHealthCompliance PSCommand_Test-PSCodeHealthCompliance PSIncludes_Function</S>
          </En>
          <En>
            <S N="Key">created</S>
            <S N="Value">5/10/2018 4:50:31 PM +00:00</S>
          </En>
          <En>
            <S N="Key">description</S>
            <S N="Value">This module allows you to measure the quality and maintainability of your PowerShell code, based on a variety of metrics.</S>
          </En>
          <En>
            <S N="Key">published</S>
            <S N="Value">5/10/2018 4:50:31 PM +00:00</S>
          </En>
          <En>
            <S N="Key">developmentDependency</S>
            <S N="Value">False</S>
          </En>
          <En>
            <S N="Key">NormalizedVersion</S>
            <S N="Value">0.2.26</S>
          </En>
          <En>
            <S N="Key">downloadCount</S>
            <S N="Value">993</S>
          </En>
          <En>
            <S N="Key">GUID</S>
            <S N="Value">ca22dabd-bbb6-4805-9c90-a8aad6dbbfd3</S>
          </En>
          <En>
            <S N="Key">PowerShellVersion</S>
            <S N="Value">5.0</S>
          </En>
          <En>
            <S N="Key">updated</S>
            <S N="Value">2018-05-11T11:56:20Z</S>
          </En>
          <En>
            <S N="Key">isLatestVersion</S>
            <S N="Value">True</S>
          </En>
          <En>
            <S N="Key">IsPrerelease</S>
            <S N="Value">false</S>
          </En>
          <En>
            <S N="Key">isAbsoluteLatestVersion</S>
            <S N="Value">True</S>
          </En>
          <En>
            <S N="Key">packageSize</S>
            <S N="Value">61337</S>
          </En>
          <En>
            <S N="Key">FileList</S>
            <S N="Value">PSCodeHealth.nuspec|PSCodeHealth.Format.ps1xml|PSCodeHealth.psd1|PSCodeHealth.psm1|PSCodeHealthSettings.json|Assets\HealthReport.css|Assets\HealthReport.html|Assets\HealthReport.js|Assets\PSCodeHealthLogo.png|Private\Get-ExternalHelpCommand.ps1|Private\Get-PowerShellFile.ps1|Private\Merge-PSCodeHealthSetting.ps1|Private\New-FailedTestsInfo.ps1|Private\New-FunctionHealthRecord.ps1|Private\New-PSCodeHealthComplianceResult.ps1|Private\New-PSCodeHealthComplianceRule.ps1|Private\New-PSCodeHealthReport.ps1|Private\Write-VerboseOutput.ps1|Private\HtmlReport\New-PSCodeHealthTableData.ps1|Private\HtmlReport\Set-PSCodeHealthHtmlColor.ps1|Private\HtmlReport\Set-PSCodeHealthPlaceholdersValue.ps1|Private\Metrics\Get-FunctionDefinition.ps1|Private\Metrics\Get-FunctionLinesOfCode.ps1|Private\Metrics\Get-FunctionScriptAnalyzerResult.ps1|Private\Metrics\Get-FunctionTestCoverage.ps1|Private\Metrics\Get-SwitchCombination.ps1|Private\Metrics\Measure-FunctionComplexity.ps1|Private\Metrics\Measure-FunctionForCodePath.ps1|Private\Metrics\Measure-FunctionIfCodePath.ps1|Private\Metrics\Measure-FunctionLogicalOpCodePath.ps1|Private\Metrics\Measure-FunctionMaxNestingDepth.ps1|Private\Metrics\Measure-FunctionSwitchCodePath.ps1|Private\Metrics\Measure-FunctionTrapCatchCodePath.ps1|Private\Metrics\Measure-FunctionWhileCodePath.ps1|Private\Metrics\Test-FunctionHelpCoverage.ps1|Public\Get-PSCodeHealthComplianceRule.ps1|Public\Invoke-PSCodeHealth.ps1|Public\Test-PSCodeHealthCompliance.ps1</S>
          </En>
          <En>
            <S N="Key">requireLicenseAcceptance</S>
            <S N="Value">True</S>
          </En>
        </DCT>
      </Obj>
      <S N="InstalledLocation">C:\Users\appveyor\AppData\Local\Temp\1\7542b18a-2d91-449b-9b66-81e735cee295\PSCodeHealth\0.2.26</S>
    </MS>
  </Obj>
</Objs>
tools\PSCodeHealth\Public\Get-PSCodeHealthComplianceRule.ps1
Function Get-PSCodeHealthComplianceRule {
<#
.SYNOPSIS
    Get the PSCodeHealth compliance rules (metrics thresholds, etc...) which are currently in effect.  

.DESCRIPTION
    Get the PSCodeHealth compliance rules (metrics warning and fail thresholds, etc...) which are currently in effect.  
    By default, all the compliance rules are coming from the file PSCodeHealthSettings.json in the module root.  

    Custom compliance rules can be specified in JSON format in a file, via the parameter CustomSettingsPath.  
    In this case, any compliance rules specified in the custom settings file override the default, and rules not specified in the custom settings file will use the defaults from PSCodeHealthSettings.json.  

    By default, this function outputs compliance rules for every metrics in every settings groups, but this can filtered via the MetricName and the SettingsGroup parameters.  

.PARAMETER CustomSettingsPath
    To specify the path of a file containing user-defined compliance rules (metrics thresholds, etc...) in JSON format.  
    Any compliance rule specified in this file override the default, and rules not specified in this file will use the default from PSCodeHealthSettings.json.  

.PARAMETER SettingsGroup
    To filter the output compliance rules to only the ones located in the specified group.  
    There are 2 settings groups in PSCodeHealthSettings.json, so there are 2 possible values for this parameter : 'PerFunctionMetrics' and 'OverallMetrics'.  
    Metrics in the PerFunctionMetrics group are generated for each individual function and metrics in the OverallMetrics group are calculated for the entire file or folder specified in the 'Path' parameter of Invoke-PSCodeHealth.  
    If not specified, compliance rules from both groups are output.  

.PARAMETER MetricName
    To filter the output compliance rules to only the ones for the specified metric or metrics.  
    There is a large number of metrics, so for convenience, all the possible values are available via tab completion.

.EXAMPLE
    PS C:\> Get-PSCodeHealthComplianceRule

    Gets all the default PSCodeHealth compliance rules (metrics warning and fail thresholds, etc...).

.EXAMPLE
    PS C:\> Get-PSCodeHealthComplianceRule -CustomSettingsPath .\MySettings.json -SettingsGroup OverallMetrics

    Gets all PSCodeHealth compliance rules (metrics warning and fail thresholds, etc...) in effect in the group 'OverallMetrics'.  
    This also output any compliance rule overriding the defaults because they are specified in the file MySettings.json.

.EXAMPLE
    PS C:\> Get-PSCodeHealthComplianceRule -MetricName 'TestCoverage','Complexity','MaximumNestingDepth'

    Gets the default compliance rules in effect for the TestCoverage, Complexity and MaximumNestingDepth metrics.  
    In the case of TestCoverage, this metric exists in both PerFunctionMetrics and OverallMetrics, so the TestCoverage compliance rules from both groups will be output.  

.OUTPUTS
    PSCodeHealth.Compliance.Rule
#>
    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    Param(
        [Parameter(Mandatory=$False,Position=0)]
        [ValidateScript({ Test-Path -Path $_ -PathType Leaf })]
        [string]$CustomSettingsPath,

        [Parameter(Mandatory=$False,Position=1)]
        [ValidateSet('PerFunctionMetrics','OverallMetrics')]
        [string]$SettingsGroup,

        [Parameter(Mandatory=$False,Position=2)]
        [ValidateSet('LinesOfCode','ScriptAnalyzerFindings','TestCoverage','CommandsMissed','Complexity','MaximumNestingDepth','LinesOfCodeTotal',
        'LinesOfCodeAverage','ScriptAnalyzerFindingsTotal','ScriptAnalyzerErrors','ScriptAnalyzerWarnings',
        'ScriptAnalyzerInformation','ScriptAnalyzerFindingsAverage','NumberOfFailedTests','TestsPassRate',
        'CommandsMissedTotal','ComplexityAverage','ComplexityHighest','NestingDepthAverage','NestingDepthHighest')]
        [string[]]$MetricName
    )

    $MetricsGroups = @('PerFunctionMetrics','OverallMetrics')
    $DefaultSettingsPath = "$PSScriptRoot\..\PSCodeHealthSettings.json"
    $DefaultSettings = ConvertFrom-Json (Get-Content -Path $DefaultSettingsPath -Raw) -ErrorAction Stop | Where-Object { $_ }

    If ( $PSBoundParameters.ContainsKey('CustomSettingsPath') ) {
        Try {
            $CustomSettings = ConvertFrom-Json (Get-Content -Path $CustomSettingsPath -Raw) -ErrorAction Stop | Where-Object { $_ }
        }
        Catch {
            Throw "An error occurred when attempting to convert JSON data from the file $CustomSettingsPath to an object. Please verify that the content of this file is in valid JSON format."
        }
    }    
    If ( $CustomSettings ) {
        $SettingsInEffect = Merge-PSCodeHealthSetting -DefaultSettings $DefaultSettings -CustomSettings $CustomSettings
    }
    Else {
        $SettingsInEffect = $DefaultSettings
    }

    If ( $PSBoundParameters.ContainsKey('SettingsGroup') ) {
        If ( $PSBoundParameters.ContainsKey('MetricName') ) {
            $MetricsInGroup = $SettingsInEffect.$($SettingsGroup) | Where-Object { ($_ | Get-Member -MemberType Properties).Name -in $MetricName }
            Write-VerboseOutput "Found $($MetricsInGroup.Count) relevant metrics in the group $SettingsGroup"
            Foreach ( $MetricRule in $MetricsInGroup ) {
                New-PSCodeHealthComplianceRule -MetricRule $MetricRule -SettingsGroup $SettingsGroup
            }
        }
        Else {
            $MetricsInGroup = $SettingsInEffect.$($SettingsGroup)
            Write-VerboseOutput "Found $($MetricsInGroup.Count) relevant metrics in the group $SettingsGroup"
            Foreach ( $MetricRule in $MetricsInGroup ) {
                New-PSCodeHealthComplianceRule -MetricRule $MetricRule -SettingsGroup $SettingsGroup
            }
        }
    }
    Else {
        If ( $PSBoundParameters.ContainsKey('MetricName') ) {
            Foreach ( $MetricGroup in $MetricsGroups ) {

                $MetricsInGroup = $SettingsInEffect.$($MetricGroup) | Where-Object { ($_ | Get-Member -MemberType Properties).Name -in $MetricName }
                Write-VerboseOutput "Found $($MetricsInGroup.Count) relevant metrics in the group $MetricGroup"
                Foreach ( $MetricRule in $MetricsInGroup ) {
                    New-PSCodeHealthComplianceRule -MetricRule $MetricRule -SettingsGroup $MetricGroup
                }
            }
        }
        Else {
            Foreach ( $MetricGroup in $MetricsGroups ) {

                $MetricsInGroup = $SettingsInEffect.$($MetricGroup)
                Write-VerboseOutput "Found $($MetricsInGroup.Count) relevant metrics in the group $MetricGroup"
                Foreach ( $MetricRule in $MetricsInGroup ) {
                    New-PSCodeHealthComplianceRule -MetricRule $MetricRule -SettingsGroup $MetricGroup
                }
            }
        }
    }
}
tools\PSCodeHealth\Public\Invoke-PSCodeHealth.ps1
Function Invoke-PSCodeHealth {
<#
.SYNOPSIS
    Gets quality and maintainability metrics for PowerShell code contained in scripts, modules or directories.

.DESCRIPTION
    Gets quality and maintainability metrics for PowerShell code contained in scripts, modules or directories.
    These metrics relate to :  
      - Length of functions  
      - Complexity of functions  
      - Code smells, styling issues and violations of best practices (using PSScriptAnalyzer)  
      - Tests and test coverage (using Pester to run tests)  
      - Comment-based help in functions  

.PARAMETER Path
    To specify the path of the directory to search for PowerShell files to analyze.  
    If the Path is not specified and the current location is in a FileSystem PowerShell drive, this will default to the current directory.

.PARAMETER TestsPath
    To specify the file or directory where tests are located.  
    If not specified, the command will look for tests in the same directory as each function.

.PARAMETER TestsResult
    To use an existing Pester tests result object for generating the following metrics :  
      - NumberOfTests  
      - NumberOfFailedTests  
      - NumberOfPassedTests  
      - TestsPassRate (%)  
      - TestCoverage (%)  
      - CommandsMissedTotal  

.PARAMETER Recurse
    To search PowerShell files in the Path directory and all subdirectories recursively.

.PARAMETER Exclude
    To specify file(s) to exclude from both the code analysis point of view and the test coverage point of view.  
    The value of this parameter qualifies the Path parameter.  
    Enter a path element or pattern, such as *example*. Wildcards are permitted.

.PARAMETER HtmlReportPath
    To instruct Invoke-PSCodeHealth to generate an HTML report, and specify the path where the HTML file should be saved.  
    The path must include the folder path (which has to exist) and the file name.  

.PARAMETER CustomSettingsPath
    To specify the path of a file containing user-defined compliance rules (metrics thresholds, etc...) in JSON format.  
    Any compliance rule specified in this file override the default, and rules not specified in this file will use the default from PSCodeHealthSettings.json.  

.PARAMETER PassThru
    When the parameter HtmlReportPath is used, by default, Invoke-PSCodeHealth doesn't output a [PSCodeHealth.Overall.HealthReport] object to the pipeline.  
    The PassThru parameter allows to instruct Invoke-PSCodeHealth to output both an HTML report file and a [PSCodeHealth.Overall.HealthReport] object.  

.EXAMPLE
    PS C:\> Invoke-PSCodeHealth -Path 'C:\GitRepos\MyModule' -Recurse -TestsPath 'C:\GitRepos\MyModule\Tests\Unit'

    Gets quality and maintainability metrics for code from PowerShell files in the directory C:\GitRepos\MyModule\ and any subdirectories.  
    This command will look for tests located in the directory C:\GitRepos\MyModule\Tests\Unit, and any subdirectories.

.EXAMPLE
    PS C:\> Invoke-PSCodeHealth -Path 'C:\GitRepos\MyModule' -TestsPath 'C:\GitRepos\MyModule\Tests' -Recurse -Exclude "*example*"

    Gets quality and maintainability metrics for code from PowerShell files in the directory C:\GitRepos\MyModule\ and any subdirectories, except for files containing "example" in their name.  
    This command will look for tests located in the directory C:\GitRepos\MyModule\Tests\, and any subdirectories.

.EXAMPLE
    PS C:\> Invoke-PSCodeHealth -Path 'C:\GitRepos\MyModule' -TestsPath 'C:\GitRepos\MyModule\Tests' -HtmlReportPath .\Report.html -PassThru

    Gets quality and maintainability metrics for code from PowerShell files in the directory C:\GitRepos\MyModule\.  
    This command will create an HTML report (Report.html) in the current directory and a PSCodeHealth.Overall.HealthReport object to the pipeline.  
    The styling of HTML elements will reflect their compliance, based on the default compliance rules.

.EXAMPLE
    PS C:\> Invoke-PSCodeHealth -Path 'C:\GitRepos\MyModule' -TestsPath 'C:\GitRepos\MyModule\Tests' -HtmlReportPath .\Report.html -CustomSettingsPath .\MySettings.json

    Gets quality and maintainability metrics for code from PowerShell files in the directory C:\GitRepos\MyModule\.  
    This command will create an HTML report (Report.html) in the current directory and a PSCodeHealth.Overall.HealthReport object to the pipeline.  
    The styling of HTML elements will reflect their compliance, based on the default compliance rules and any custom rules in the file .\MySettings.json.


.OUTPUTS
    PSCodeHealth.Overall.HealthReport

.NOTES
    
#>
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [OutputType([PSCustomObject])]
    Param (
        [Parameter(Position=0, Mandatory=$False, ValueFromPipeline=$True)]
        [ValidateScript({ Test-Path $_ })]
        [string]$Path,

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

        [Parameter(Position=2, Mandatory=$False)]
        [ValidateScript({ $_.TotalCount -is [int] })]
        [PSCustomObject]$TestsResult,

        [switch]$Recurse,

        [Parameter(Mandatory=$False)]
        [string[]]$Exclude,

        [Parameter(Mandatory, ParameterSetName='HtmlReport')]
        [ValidateScript({ Test-Path -Path (Split-Path $_ -Parent) -PathType Container })]
        [string]$HtmlReportPath,

        [Parameter(Mandatory=$False, ParameterSetName='HtmlReport')]
        [ValidateScript({ Test-Path -Path $_ -PathType Leaf })]
        [string]$CustomSettingsPath,

        [Parameter(Mandatory=$False, ParameterSetName='HtmlReport')]
        [switch]$PassThru

    )
    If ( $PSBoundParameters.ContainsKey('Path') ) {
        $Path = (Resolve-Path -Path $Path).Path
    }
    Else {
        If ( $PWD.Provider.Name -eq 'FileSystem' ) {
            $Path = $PWD.ProviderPath
        }
        Else {
            Throw "The current location is from the $($PWD.Provider.Name) provider, please provide a value for the Path parameter or change to a FileSystem location."
        }
    }
    
    If ( (Get-Item -Path $Path).PSIsContainer ) {
        $ExternalHelpSearchRoot = $Path
        If ( $PSBoundParameters.ContainsKey('Exclude') ) {
            $PowerShellFiles = Get-PowerShellFile -Path $Path -Recurse:$($Recurse.IsPresent) -Exclude $Exclude
        }
        Else {
            $PowerShellFiles = Get-PowerShellFile -Path $Path -Recurse:$($Recurse.IsPresent)
        }
    }
    Else {
        $ExternalHelpSearchRoot = Split-Path -Path $Path -Parent
        $PowerShellFiles = $Path
    }

    If ( -not $PowerShellFiles ) {
        return $Null
    }
    Else {
        Write-VerboseOutput -Message 'Found the following PowerShell files in the directory :'
        Write-VerboseOutput -Message "$($PowerShellFiles | Out-String)"
    }
    $Script:ExternalHelpCommandNames = Get-ExternalHelpCommand -Path $ExternalHelpSearchRoot

    $FunctionDefinitions = Get-FunctionDefinition -Path $PowerShellFiles
    [System.Collections.ArrayList]$FunctionHealthRecords = @()

    If ( -not $FunctionDefinitions ) {
        $FunctionHealthRecords = $Null
    }
    Else {
        Foreach ( $Function in $FunctionDefinitions ) {

            Write-VerboseOutput -Message "Gathering metrics for function : $($Function.Name)"

            $TestCoverageParams = If ( $TestsPath ) {
                @{ FunctionDefinition = $Function; TestsPath = $TestsPath }} Else {
                @{ FunctionDefinition = $Function }
            }
            $TestCoverage = Get-FunctionTestCoverage @TestCoverageParams

            $FunctionHealthRecord = New-FunctionHealthRecord -FunctionDefinition $Function -FunctionTestCoverage $TestCoverage
            $Null = $FunctionHealthRecords.Add($FunctionHealthRecord)
        }
    }

    If ( -not $TestsPath ) {
        $TestsPath = If ( (Get-Item -Path $Path).PSIsContainer ) { $Path } Else { Split-Path -Path $Path -Parent }
    }
    $PathItem = (Get-Item -Path $Path)
    $ReportTitle = $PathItem.Name
    $AnalyzedPath = $PathItem.FullName

    $PSCodeHealthReportParams = @{
        ReportTitle = $ReportTitle
        AnalyzedPath = $AnalyzedPath
        Path = $PowerShellFiles
        FunctionHealthRecord = $FunctionHealthRecords
        TestsPath = $TestsPath
    }
    If ( ($PSBoundParameters.ContainsKey('TestsResult')) ) {
        $HealthReport = New-PSCodeHealthReport @PSCodeHealthReportParams -TestsResult $PSBoundParameters.TestsResult
    }
    Else {
        $HealthReport = New-PSCodeHealthReport @PSCodeHealthReportParams
    }

    If ( $PSCmdlet.ParameterSetName -ne 'HtmlReport' ) {
        return $HealthReport
    }
    Else {
        $JsPlaceholders = @{
            NUMBER_OF_PASSED_TESTS = $HealthReport.NumberOfPassedTests
            NUMBER_OF_FAILED_TESTS = $HealthReport.NumberOfFailedTests
            TESTS_PASS_RATE = $HealthReport.TestsPassRate
            TEST_COVERAGE = $HealthReport.TestCoverage
            CODE_NOT_COVERED = 100 - $HealthReport.TestCoverage
        }
        $JsContent = Set-PSCodeHealthPlaceholdersValue -TemplatePath "$PSScriptRoot\..\Assets\HealthReport.js" -PlaceholdersData $JsPlaceholders

        $TableData = New-PSCodeHealthTableData -HealthReport $HealthReport

        $HtmlPlaceholders = @{
            REPORT_TITLE = $HealthReport.ReportTitle
            CSS_CONTENT = Get-Content -Path "$PSScriptRoot\..\Assets\HealthReport.css"
            ANALYZED_PATH = $HealthReport.AnalyzedPath
            REPORT_DATE = $HealthReport.ReportDate
            NUMBER_OF_FILES = $HealthReport.Files
            NUMBER_OF_FUNCTIONS = $HealthReport.Functions
            LINES_OF_CODE_TOTAL = $HealthReport.LinesOfCodeTotal
            SCRIPTANALYZER_ERRORS = $HealthReport.ScriptAnalyzerErrors
            SCRIPTANALYZER_WARNINGS = $HealthReport.ScriptAnalyzerWarnings
            SCRIPTANALYZER_INFO = $HealthReport.ScriptAnalyzerInformation
            SCRIPTANALYZER_TOTAL = $HealthReport.ScriptAnalyzerFindingsTotal
            SCRIPTANALYZER_AVERAGE = $HealthReport.ScriptAnalyzerFindingsAverage
            FUNCTIONS_WITHOUT_HELP = $HealthReport.FunctionsWithoutHelp
            BEST_PRACTICES_TABLE_ROWS = $TableData.BestPracticesRows
            COMPLEXITY_HIGHEST = $HealthReport.ComplexityHighest
            NESTING_DEPTH_HIGHEST = $HealthReport.NestingDepthHighest
            LINES_OF_CODE_AVERAGE = $HealthReport.LinesOfCodeAverage
            COMPLEXITY_AVERAGE = $HealthReport.ComplexityAverage
            NESTING_DEPTH_AVERAGE = $HealthReport.NestingDepthAverage
            MAINTAINABILITY_TABLE_ROWS = $TableData.MaintainabilityRows
            NUMBER_OF_TESTS = $HealthReport.NumberOfTests
            NUMBER_OF_FAILED_TESTS = $HealthReport.NumberOfFailedTests
            NUMBER_OF_PASSED_TESTS = $HealthReport.NumberOfPassedTests
            COMMANDS_MISSED = $HealthReport.CommandsMissedTotal
            FAILED_TESTS_TABLE_ROWS = $TableData.FailedTestsRows
            COVERAGE_TABLE_ROWS = $TableData.CoverageRows
            JS_CONTENT = $JsContent
        }
        $HtmlContent = Set-PSCodeHealthPlaceholdersValue -TemplatePath "$PSScriptRoot\..\Assets\HealthReport.html" -PlaceholdersData $HtmlPlaceholders

        $ComplianceParams = @{
            HealthReport = $HealthReport                
        }
        If ( $PSBoundParameters.ContainsKey('CustomSettingsPath') ) {
            $ComplianceParams.Add('CustomSettingsPath', $CustomSettingsPath)
        }
        $OverallCompliance = Test-PSCodeHealthCompliance @ComplianceParams
        If ( $Null -eq $FunctionHealthRecords ) {
            $PerFunctionCompliance = $Null
        }
        Else {
            $PerFunctionCompliance = $FunctionHealthRecords.FunctionName.ForEach({ Test-PSCodeHealthCompliance @ComplianceParams -FunctionName $_ })
        }

        $HtmlColorParams = @{
            HealthReport = $HealthReport
            Compliance = $OverallCompliance
            PerFunctionCompliance = $PerFunctionCompliance
            Html = $HtmlContent
        }
        $ColoredHtmlContent = Set-PSCodeHealthHtmlColor @HtmlColorParams

        $Null = New-Item -Path $HtmlReportPath -ItemType File -Force
        Set-Content -Path $HtmlReportPath -Value $ColoredHtmlContent
        If ( $PassThru ) {
            return $HealthReport
        }
    }
}
tools\PSCodeHealth\Public\Test-PSCodeHealthCompliance.ps1
Function Test-PSCodeHealthCompliance {
<#
.SYNOPSIS
    Gets the compliance result(s) of the analyzed PowerShell code, based on a PSCodeHealth report and compliance rules contained in PSCodeHealth settings.  

.DESCRIPTION
    Gets the compliance result(s) of the analyzed PowerShell code, based on a PSCodeHealth report and compliance rules contained in PSCodeHealth settings.  
    The values in the input PSCodeHealth report will be checked for compliance against the rules in the PSCodeHealth settings which are currently in effect.  
    By default, all compliance rules are coming from the file PSCodeHealthSettings.json in the module root. Custom compliance rules can be specified in JSON format in a file, via the parameter CustomSettingsPath.  

    The possible compliance levels are :  
      - Pass  
      - Warning  
      - Fail  
    
    By default, this function outputs the compliance results for every metrics in every settings groups, but this can filtered via the MetricName and the SettingsGroup parameters.  

.PARAMETER HealthReport
    The PSCodeHealth report (object of the type PSCodeHealth.Overall.HealthReport) to analyze for compliance.  
    The ouput of the command Invoke-PSCodeHealth is a PSCodeHealth report and can be bound to this parameter via pipeline input.  

.PARAMETER CustomSettingsPath
    To specify the path of a file containing user-defined compliance rules (metrics thresholds, etc...) in JSON format.  
    Any compliance rule specified in this file override the default, and rules not specified in this file will use the default from PSCodeHealthSettings.json.  

.PARAMETER SettingsGroup
    To evaluate compliance only for the metrics located in the specified group.  
    There are 2 settings groups in PSCodeHealthSettings.json, so there are 2 possible values for this parameter : 'PerFunctionMetrics' and 'OverallMetrics'.  
    Metrics in the PerFunctionMetrics group are for each individual function and metrics in the OverallMetrics group are for the entire file or folder specified in the 'Path' parameter of Invoke-PSCodeHealth.  
    If not specified, compliance is evaluated for metrics in both groups.  

.PARAMETER MetricName
    To get compliance results only for the specified metric(s).
    There is a large number of metrics, so for convenience, all the possible values are available via tab completion.
    If not specified, compliance is evaluated for all metrics.

.PARAMETER FunctionName
    To get compliance results for a specific function.  
    This is a dynamic parameter which is available when the specified HealthReport contains at least 1 FunctionHealthRecords.  

.PARAMETER Summary
    To output a single overall compliance result based on all the evaluated metrics.  
    This retains the worst compliance level, meaning :  
      - If any evaluated metric has the 'Fail' compliance level, the overall result is 'Fail'  
      - If any evaluated metric has the 'Warning' compliance level and none has 'Fail', the overall result is 'Warning'  
      - If all evaluated metrics has the 'Pass' compliance level, the overall result is 'Pass'  

.EXAMPLE
    PS C:\> Test-PSCodeHealthCompliance -HealthReport $MyProjectHealthReport

    Gets the compliance results for every metrics, based on the specified PSCodeHealth report ($MyProjectHealthReport) and the compliance rules in the default settings.

.EXAMPLE
    PS C:\> Invoke-PSCodeHealth | Test-PSCodeHealthCompliance

    Gets the compliance results for every metrics, based on the PSCodeHealth report specified via pipeline input and the compliance rules in the default settings.

.EXAMPLE
    PS C:\> Test-PSCodeHealthCompliance -HealthReport $MyProjectHealthReport -CustomSettingsPath .\MySettings.json -SettingsGroup OverallMetrics

    Evaluates the compliance results for the metrics in the settings group OverallMetrics, based on the specified PSCodeHealth report ($MyProjectHealthReport).  
    This checks compliance against compliance rules in the defaults compliance rules and any custom compliance rule from the file 'MySettings.json'.  

.EXAMPLE
    PS C:\> Test-PSCodeHealthCompliance -HealthReport $MyProjectHealthReport -MetricName 'TestCoverage','Complexity','MaximumNestingDepth'

    Evaluates the compliance results only for the TestCoverage, Complexity and MaximumNestingDepth metrics.  
    In the case of TestCoverage, this metric exists in both PerFunctionMetrics and OverallMetrics, so this evaluates the compliance result for the TestCoverage metric from both groups.  

.EXAMPLE
    PS C:\> Test-PSCodeHealthCompliance -HealthReport $MyProjectHealthReport -FunctionName 'Get-Something'

    Evaluates the compliance results specifically for the function Get-Something. Because this is the compliance of a specific function, only the per function metrics are evaluated.  
    If the value of the FunctionName parameter doesn't match any function name in the HealthReport the parameter validation will fail and state the set of possible values.  

.EXAMPLE
    PS C:\> Invoke-PSCodeHealth | Test-PSCodeHealthCompliance -Summary

    Evaluates the compliance results for every metrics, based on the PSCodeHealth report specified via pipeline input and the compliance rules in the default settings.  
    This outputs an overall 'Fail','Warning' or 'Pass' value for all the evaluated metrics.


.OUTPUTS
    PSCodeHealth.Compliance.Result, PSCodeHealth.Compliance.FunctionResult, System.String
#>
    [CmdletBinding()]
    [OutputType([PSCustomObject[]], [string])]
    Param(
        [Parameter(Mandatory, Position=0, ValueFromPipeline=$True)]
        [PSTypeName('PSCodeHealth.Overall.HealthReport')]
        [PSCustomObject]$HealthReport,

        [Parameter(Mandatory=$False,Position=1)]
        [ValidateScript({ Test-Path -Path $_ -PathType Leaf })]
        [string]$CustomSettingsPath,

        [Parameter(Mandatory=$False,Position=2)]
        [ValidateSet('PerFunctionMetrics','OverallMetrics')]
        [string]$SettingsGroup,

        [Parameter(Mandatory=$False,Position=3)]
        [ValidateSet('LinesOfCode','ScriptAnalyzerFindings','TestCoverage','CommandsMissed','Complexity','MaximumNestingDepth','LinesOfCodeTotal',
        'LinesOfCodeAverage','ScriptAnalyzerFindingsTotal','ScriptAnalyzerErrors','ScriptAnalyzerWarnings',
        'ScriptAnalyzerInformation','ScriptAnalyzerFindingsAverage','NumberOfFailedTests','TestsPassRate',
        'CommandsMissedTotal','ComplexityAverage','ComplexityHighest','NestingDepthAverage','NestingDepthHighest')]
        [string[]]$MetricName,

        [Parameter(Mandatory=$False)]
        [switch]$Summary
    )

    DynamicParam {
        # The FunctionName parameter is dynamic because the set of possible values depends on the FunctionHealthRecords contained in the specified HealthReport.
        If ( $HealthReport.FunctionHealthRecords.Count -gt 0 ) {
            
            $ParameterName = 'FunctionName'            
            # Creating a parameter dictionary 
            $RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

            $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute]            
            $ValidationScriptAttribute = New-Object -TypeName System.Management.Automation.ParameterAttribute
            $ValidationScriptAttribute.Mandatory = $False
            $AttributeCollection.Add($ValidationScriptAttribute)
            # Generating dynamic values for a ValidateSet
            $SetValues = $HealthReport.FunctionHealthRecords.FunctionName
            $ValidateSetAttribute = New-Object -TypeName System.Management.Automation.ValidateSetAttribute($SetValues)
            # Adding the ValidateSet to the attributes collection
            $AttributeCollection.Add($ValidateSetAttribute)
            $RuntimeParameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
            $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
            return $RuntimeParameterDictionary
        }
    }
    
    Begin {
        If ( $RuntimeParameterDictionary ) { $FunctionName = $RuntimeParameterDictionary[$ParameterName].Value }
        $Null = $PSBoundParameters.Remove('HealthReport')
        If ( $PSBoundParameters.ContainsKey('Summary') ) {
            $Null = $PSBoundParameters.Remove('Summary')
        }
        If ( $PSBoundParameters.ContainsKey('FunctionName') ) {
            $Null = $PSBoundParameters.Remove('FunctionName')
        }        
        [System.Collections.ArrayList]$ComplianceResults = @()
        $ComplianceRules = Get-PSCodeHealthComplianceRule @PSBoundParameters
        Write-VerboseOutput "Evaluating the specified health report against $($ComplianceRules.Count) compliance rules."
    }

    Process {
        $FunctionHealthRecords = If ($FunctionName) {$HealthReport.FunctionHealthRecords | Where-Object FunctionName -eq $FunctionName} Else {$HealthReport.FunctionHealthRecords}

        Foreach ( $ComplianceRule in $ComplianceRules ) {
            If ( $ComplianceRule.SettingsGroup -eq 'PerFunctionMetrics' ) {

                $MetricsFromReport = $FunctionHealthRecords.$($ComplianceRule.MetricName)
                If ( $Null -ne $MetricsFromReport ) {
                    If ( $ComplianceRule.HigherIsBetter ) {
                        # We always retain the worst value of all the analyzed functions
                        $RetainedValue = ($MetricsFromReport | Measure-Object -Minimum).Minimum
                        Write-VerboseOutput "Retained value for $($ComplianceRule.MetricName) : $($RetainedValue)"

                        Switch ($RetainedValue) {
                            { $_ -lt $ComplianceRule.FailThreshold } { $ComplianceResult = 'Fail'; break}
                            { $_ -lt $ComplianceRule.WarningThreshold } { $ComplianceResult = 'Warning'; break}
                            Default { $ComplianceResult = 'Pass' }
                        }

                        $ResultParams = @{
                            ComplianceRule = $ComplianceRule
                            Value = $RetainedValue
                            Result = $ComplianceResult
                        }
                        If ( $FunctionName ) {
                            $ComplianceResultObj = New-PSCodeHealthComplianceResult @ResultParams -FunctionName $FunctionName
                        }
                        Else {
                            $ComplianceResultObj = New-PSCodeHealthComplianceResult @ResultParams
                        }
                        $Null = $ComplianceResults.Add($ComplianceResultObj)
                    }
                    Else {
                        # We always retain the worst value of all the analyzed functions
                        $RetainedValue = ($MetricsFromReport | Measure-Object -Maximum).Maximum
                        Write-VerboseOutput "Retained value for $($ComplianceRule.MetricName) : $($RetainedValue)"

                        Switch ($RetainedValue) {
                            { $_ -gt $ComplianceRule.FailThreshold } { $ComplianceResult = 'Fail'; break}
                            { $_ -gt $ComplianceRule.WarningThreshold } { $ComplianceResult = 'Warning'; break}
                            Default { $ComplianceResult = 'Pass' }
                        }

                        $ResultParams = @{
                            ComplianceRule = $ComplianceRule
                            Value = $RetainedValue
                            Result = $ComplianceResult
                        }
                        If ( $FunctionName ) {
                            $ComplianceResultObj = New-PSCodeHealthComplianceResult @ResultParams -FunctionName $FunctionName
                        }
                        Else {
                            $ComplianceResultObj = New-PSCodeHealthComplianceResult @ResultParams
                        }
                        $Null = $ComplianceResults.Add($ComplianceResultObj)
                    }
                }
            }
            ElseIf ( $ComplianceRule.SettingsGroup -eq 'OverallMetrics' -and -not($FunctionName) ) {
                $MetricFromReport = $HealthReport.$($ComplianceRule.MetricName)
                If ( $MetricFromReport -or $MetricFromReport -eq 0 ) {
                    If ( $ComplianceRule.HigherIsBetter ) {

                        Switch ($MetricFromReport) {
                            { $_ -lt $ComplianceRule.FailThreshold } { $ComplianceResult = 'Fail'; break}
                            { $_ -lt $ComplianceRule.WarningThreshold } { $ComplianceResult = 'Warning'; break}
                            Default { $ComplianceResult = 'Pass' }
                        }
                        $ComplianceResultObj = New-PSCodeHealthComplianceResult -ComplianceRule $ComplianceRule -Value $MetricFromReport -Result $ComplianceResult
                        $Null = $ComplianceResults.Add($ComplianceResultObj)
                    }
                    Else {

                        Switch ($MetricFromReport) {
                            { $_ -gt $ComplianceRule.FailThreshold } { $ComplianceResult = 'Fail'; break}
                            { $_ -gt $ComplianceRule.WarningThreshold } { $ComplianceResult = 'Warning'; break}
                            Default { $ComplianceResult = 'Pass' }
                        }
                        $ComplianceResultObj = New-PSCodeHealthComplianceResult -ComplianceRule $ComplianceRule -Value $MetricFromReport -Result $ComplianceResult
                        $Null = $ComplianceResults.Add($ComplianceResultObj)
                    }
                }
            }
        }
    }

    End {
        If ( $Summary ) {
            If ( $ComplianceResults.Result -contains 'Fail') {
                return 'Fail'
            }
            If ( $ComplianceResults.Result -contains 'Warning') {
                return 'Warning'
            }
            return 'Pass'
        }
        return $ComplianceResults
    }
}
tools\VERIFICATION.txt
VERIFICATION
Verification is intended to assist the Chocolatey moderators and community in verifying that this package's contents are trustworthy.

To verify the files using the project source:

1. Please go to the project source location (https://github.com/MathieuBuisson/PSCodeHealth) and download the source files;
2. Build the source to create the binary files to verify;
3. Use Get-FileHash -Path <FILE TO VERIFY> to get the file hash value from both the built file (from step 1 above) and the file from the package and compare them;

Alternatively you can download the module from the PowerShell Gallery ...

    Save-Module -Name PSCodeHealth -Path <PATH TO DOWNLOAD TO>

... and compare the files from the package against those in the installed module. Again use Get-FileHash -Path <FILE TO VERIFY> to retrieve those hash values.

Log in or click on link to see number of positives.

In cases where actual malware is found, the packages are subject to removal. Software sometimes has false positives. Moderators do not necessarily validate the safety of the underlying software, only that a package retrieves software from the official distribution point and/or validate embedded software against official distribution point (where distribution rights allow redistribution).

Chocolatey Pro provides runtime protection from possible malware.

Add to Builder Version Downloads Last Updated Status
PSCodeHealth (PowerShell Module) 0.2.9 323 Thursday, May 10, 2018 Approved
Discussion for the PSCodeHealth (PowerShell Module) Package

Ground Rules:

  • This discussion is only about PSCodeHealth (PowerShell Module) and the PSCodeHealth (PowerShell Module) package. If you have feedback for Chocolatey, please contact the Google Group.
  • This discussion will carry over multiple versions. If you have a comment about a particular version, please note that in your comments.
  • The maintainers of this Chocolatey Package will be notified about new comments that are posted to this Disqus thread, however, it is NOT a guarantee that you will get a response. If you do not hear back from the maintainers after posting a message below, please follow up by using the link on the left side of this page or follow this link to contact maintainers. If you still hear nothing back, please follow the package triage process.
  • Tell us what you love about the package or PSCodeHealth (PowerShell Module), or tell us what needs improvement.
  • Share your experiences with the package, or extra configuration or gotchas that you've found.
  • If you use a url, the comment will be flagged for moderation until you've been whitelisted. Disqus moderated comments are approved on a weekly schedule if not sooner. It could take between 1-5 days for your comment to show up.
comments powered by Disqus