diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index 7f7a76a41..de6306f05 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -243,7 +243,19 @@ public SwitchParameter AttachAndDebug set { attachAndDebug = value; } } private bool attachAndDebug = false; + #endif + /// + /// Write a summary of rule violations to the host, which might be undesirable in some cases, therefore this switch is optional. + /// + [Parameter(Mandatory = false)] + public SwitchParameter ReportSummary + { + get { return reportSummary; } + set { reportSummary = value; } + } + private SwitchParameter reportSummary; + #endregion Parameters #region Overrides @@ -424,9 +436,49 @@ private void WriteToOutput(IEnumerable diagnosticRecords) { foreach (ILogger logger in ScriptAnalyzer.Instance.Loggers) { + var errorCount = 0; + var warningCount = 0; + var infoCount = 0; + foreach (DiagnosticRecord diagnostic in diagnosticRecords) { logger.LogObject(diagnostic, this); + switch (diagnostic.Severity) + { + case DiagnosticSeverity.Information: + infoCount++; + break; + case DiagnosticSeverity.Warning: + warningCount++; + break; + case DiagnosticSeverity.Error: + errorCount++; + break; + default: + throw new ArgumentOutOfRangeException(nameof(diagnostic.Severity), $"Severity '{diagnostic.Severity}' is unknown"); + } + } + + if (ReportSummary.IsPresent) + { + var numberOfRuleViolations = infoCount + warningCount + errorCount; + if (numberOfRuleViolations == 0) + { + Host.UI.WriteLine("0 rule violations found."); + } + else + { + var pluralS = numberOfRuleViolations > 1 ? "s" : string.Empty; + var message = $"{numberOfRuleViolations} rule violation{pluralS} found. Severity distribution: {DiagnosticSeverity.Error} = {errorCount}, {DiagnosticSeverity.Warning} = {warningCount}, {DiagnosticSeverity.Information} = {infoCount}"; + if (warningCount + errorCount == 0) + { + ConsoleHostHelper.DisplayMessageUsingSystemProperties(Host, "WarningForegroundColor", "WarningBackgroundColor", message); + } + else + { + ConsoleHostHelper.DisplayMessageUsingSystemProperties(Host, "ErrorForegroundColor", "ErrorBackgroundColor", message); + } + } } } diff --git a/Engine/Generic/ConsoleHostHelper.cs b/Engine/Generic/ConsoleHostHelper.cs new file mode 100644 index 000000000..961962fd2 --- /dev/null +++ b/Engine/Generic/ConsoleHostHelper.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Management.Automation.Host; + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic +{ + internal static class ConsoleHostHelper + { + internal static void DisplayMessageUsingSystemProperties(PSHost psHost, string foregroundColorPropertyName, string backgroundPropertyName, string message) + { + var gotForegroundColor = TryGetPrivateDataConsoleColor(psHost, foregroundColorPropertyName, out ConsoleColor foregroundColor); + var gotBackgroundColor = TryGetPrivateDataConsoleColor(psHost, backgroundPropertyName, out ConsoleColor backgroundColor); + if (gotForegroundColor && gotBackgroundColor) + { + psHost.UI.WriteLine(foregroundColor: foregroundColor, backgroundColor: backgroundColor, value: message); + } + else + { + psHost.UI.WriteLine(message); + } + } + + private static bool TryGetPrivateDataConsoleColor(PSHost psHost, string propertyName, out ConsoleColor consoleColor) + { + consoleColor = default(ConsoleColor); + var property = psHost.PrivateData.Properties[propertyName]; + if (property == null) + { + return false; + } + + try + { + consoleColor = (ConsoleColor)Enum.Parse(typeof(ConsoleColor), property.Value.ToString(), true); + } + catch (InvalidCastException) + { + return false; + } + + return true; + } + } +} diff --git a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 index 2b03bc5e1..bec1134ce 100644 --- a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 +++ b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 @@ -504,4 +504,16 @@ Describe "Test -EnableExit Switch" { powershell -Command 'Import-Module PSScriptAnalyzer; Invoke-ScriptAnalyzer -ScriptDefinition gci -EnableExit' $LASTEXITCODE | Should -Be 1 } + + Describe "-ReportSummary switch" { + $reportSummaryFor1Warning = '*1 rule violation found. Severity distribution: Error = 0, Warning = 1, Information = 0*' + It "prints the correct report summary using the -NoReportSummary switch" { + $result = powershell -command 'Invoke-Scriptanalyzer -ScriptDefinition gci -ReportSummary' + "$result" | Should -BeLike $reportSummaryFor1Warning + } + It "does not print the report summary when not using -NoReportSummary switch" { + $result = powershell -command 'Invoke-Scriptanalyzer -ScriptDefinition gci' + "$result" | Should -Not -BeLike $reportSummaryFor1Warning + } + } } diff --git a/Tests/Engine/LibraryUsage.tests.ps1 b/Tests/Engine/LibraryUsage.tests.ps1 index 3bf7890bd..68f3b68ee 100644 --- a/Tests/Engine/LibraryUsage.tests.ps1 +++ b/Tests/Engine/LibraryUsage.tests.ps1 @@ -52,10 +52,13 @@ function Invoke-ScriptAnalyzer { [Parameter(Mandatory = $false)] [switch] $Fix, + + [Parameter(Mandatory = $false)] + [switch] $EnableExit, [Parameter(Mandatory = $false)] - [switch] $EnableExit - ) + [switch] $ReportSummary + ) if ($null -eq $CustomRulePath) { @@ -98,6 +101,19 @@ function Invoke-ScriptAnalyzer { } $results + + if ($ReportSummary.IsPresent) + { + if ($null -ne $results) + { + # This is not the exact message that it would print but close enough + Write-Host "$($results.Count) rule violations found. Severity distribution: Error = 1, Warning = 3, Information = 5" -ForegroundColor Red + } + else + { + Write-Host '0 rule violations found.' -ForegroundColor Green + } + } if ($EnableExit.IsPresent -and $null -ne $results) { diff --git a/docs/markdown/Invoke-ScriptAnalyzer.md b/docs/markdown/Invoke-ScriptAnalyzer.md index c742babe8..85952aafe 100644 --- a/docs/markdown/Invoke-ScriptAnalyzer.md +++ b/docs/markdown/Invoke-ScriptAnalyzer.md @@ -12,14 +12,14 @@ Evaluates a script or module based on selected best practice rules ### UNNAMED_PARAMETER_SET_1 ``` Invoke-ScriptAnalyzer [-Path] [-CustomRulePath ] [-RecurseCustomRulePath] - [-ExcludeRule ] [-IncludeRule ] [-Severity ] [-Recurse] [-SuppressedOnly] [-Fix] [-EnableExit] + [-ExcludeRule ] [-IncludeRule ] [-Severity ] [-Recurse] [-SuppressedOnly] [-Fix] [-EnableExit] [-ReportSummary] [-Settings ] ``` ### UNNAMED_PARAMETER_SET_2 ``` Invoke-ScriptAnalyzer [-ScriptDefinition] [-CustomRulePath ] [-RecurseCustomRulePath] - [-ExcludeRule ] [-IncludeRule ] [-Severity ] [-Recurse] [-SuppressedOnly] [-EnableExit] + [-ExcludeRule ] [-IncludeRule ] [-Severity ] [-Recurse] [-SuppressedOnly] [-EnableExit] [-ReportSummary] [-Settings ] ``` @@ -432,6 +432,21 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -ReportSummary +Writes a report summary of the found warnings to the host. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -Settings File path that contains user profile or hash table for ScriptAnalyzer