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- Software Specific:
- Software Site
- Software Source
- Software License
- Software Docs
- Software Issues
- Package Specific:
- Package Source
- Package outdated?
- Package broken?
- Contact Maintainers
- Contact Site Admins
- Software Vendor?
- Report Abuse
- Download
PSCodeHealth (PowerShell Module)
- 1
- 2
- 3
0.2.26 | Updated: 11 May 2018
- Software Specific:
- Software Site
- Software Source
- Software License
- Software Docs
- Software Issues
- Package Specific:
- Package Source
- Package outdated?
- Package broken?
- Contact Maintainers
- Contact Site Admins
- Software Vendor?
- Report Abuse
- Download
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
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:
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
Option 1: Cached Package (Unreliable, Requires Internet - Same As Community)-
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
-
Open Source
-
Download the package:
Download - Follow manual internalization instructions
-
-
Package Internalizer (C4B)
-
Run: (additional options)
choco download pscodehealth --internalize --source=https://community.chocolatey.org/api/v2/
-
For package and dependencies run:
choco push --source="'INTERNAL REPO URL'"
- Automate package internalization
-
Run: (additional options)
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.
This package was approved by moderator flcdrg on 13 May 2018.
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.
$ErrorActionPreference = 'Stop'
$moduleName = $env:ChocolateyPackageName # this could be different from package name
Remove-Module -Name $moduleName -Force -ErrorAction SilentlyContinue
$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
}
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.
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');
});
}
});
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
}
}
}
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
}
}
}
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
}
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
}
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
}
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
}
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
}
}
}
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
}
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
}
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
}
}
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])
}
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
}
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
}
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
}
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
}
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
}
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
}
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
}
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
}
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
}
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
}
}
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
}
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
}
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
}
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
}
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
}
#
# 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 = ''
}
#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'
{
"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
}
}
]
}
<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>
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
}
}
}
}
}
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
}
}
}
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
}
}
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.
- pscodehealth.0.2.26.nupkg (c5fcf5f26200) - ## / 60
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.26 | 4755 | Friday, May 11, 2018 | Approved | |
PSCodeHealth (PowerShell Module) 0.2.9 | 323 | Thursday, May 10, 2018 | Approved |
2017 Mathieu Buisson
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.