From 2fbea9e30eb30fb2ea9fd1246802a7407654cb4f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:33:10 +0000 Subject: [PATCH 1/3] Initial plan From 3a7fed2397dcf63ebca67fabbe63b147ae5647eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:39:20 +0000 Subject: [PATCH 2/3] Filter Invoke-ScriptAnalyzer by diagnostic severity Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- .../Commands/InvokeScriptAnalyzerCommand.cs | 2 +- Engine/ScriptAnalyzer.cs | 49 ++++++------------- Tests/Engine/CustomizedRule.tests.ps1 | 24 +++++++++ 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index bf1e740c3..0b4b6f5f7 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -146,7 +146,7 @@ public string[] IncludeRule private string[] includeRule; /// - /// IncludeRule: Array of the severity types to be enabled. + /// Severity levels of the diagnostic records to include. /// [ValidateSet("Warning", "Error", "Information", "ParseError", IgnoreCase = true)] [Parameter(Mandatory = false)] diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index 91bf188f2..7f9568259 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -1485,7 +1485,7 @@ public List AnalyzeScriptDefinition(string scriptDefinition, o var relevantParseErrors = RemoveTypeNotFoundParseErrors(errors, out List diagnosticRecords); // Add parse errors first if requested! - if ( relevantParseErrors != null && (severity == null || severity.Contains("ParseError", StringComparer.OrdinalIgnoreCase))) + if ( relevantParseErrors != null ) { var results = new List(); foreach ( ParseError parseError in relevantParseErrors ) @@ -1505,7 +1505,7 @@ public List AnalyzeScriptDefinition(string scriptDefinition, o // now, analyze the script definition diagnosticRecords.AddRange(this.AnalyzeSyntaxTree(scriptAst, scriptTokens, null, skipVariableAnalysis, emitSuppressionErrors)); - return diagnosticRecords; + return FilterDiagnosticsBySeverity(diagnosticRecords).ToList(); } /// @@ -1876,7 +1876,7 @@ private IEnumerable AnalyzeFile(string filePath) // no parsing can really be done, but there are other rules to run (specifically encoding). if ( s_aboutHelpRegex.IsMatch(Path.GetFileName(filePath)) ) { - return diagnosticRecords.Concat(this.AnalyzeSyntaxTree(scriptAst, scriptTokens, filePath)); + return FilterDiagnosticsBySeverity(diagnosticRecords.Concat(this.AnalyzeSyntaxTree(scriptAst, scriptTokens, filePath))); } // Process script @@ -1899,7 +1899,7 @@ private IEnumerable AnalyzeFile(string filePath) IEnumerable relevantParseErrors = RemoveTypeNotFoundParseErrors(errors, out diagnosticRecords); // First, add all parse errors if they've been requested - if ( relevantParseErrors != null && (severity == null || severity.Contains("ParseError", StringComparer.OrdinalIgnoreCase))) + if ( relevantParseErrors != null ) { List results = new List(); foreach ( ParseError parseError in relevantParseErrors ) @@ -1916,7 +1916,7 @@ private IEnumerable AnalyzeFile(string filePath) diagnosticRecords.AddRange(results); } - return diagnosticRecords.Concat(this.AnalyzeSyntaxTree(scriptAst, scriptTokens, filePath)); + return FilterDiagnosticsBySeverity(diagnosticRecords.Concat(this.AnalyzeSyntaxTree(scriptAst, scriptTokens, filePath))); } private bool IsModuleNotFoundError(ParseError error) @@ -1925,42 +1925,26 @@ private bool IsModuleNotFoundError(ParseError error) && error.ErrorId.Equals("ModuleNotFoundDuringParse", StringComparison.OrdinalIgnoreCase); } - private bool IsSeverityAllowed(IEnumerable allowedSeverities, IRule rule) + private bool IsDiagnosticSeverityAllowed(DiagnosticRecord diagnostic) { return severity == null - || (allowedSeverities != null - && rule != null - && HasGetSeverity(rule) - && allowedSeverities.Contains((uint)rule.GetSeverity())); + || (diagnostic != null + && severity.Contains(diagnostic.Severity.ToString(), StringComparer.OrdinalIgnoreCase)); } - private static bool IsRuleEnabled(IRule rule) - { - var configurableRule = rule as ConfigurableRule; - return configurableRule == null || configurableRule.Enable; - } - - IEnumerable GetAllowedSeveritiesInInt() - { - return severity != null - ? severity.Select(item => (uint)Enum.Parse(typeof(DiagnosticSeverity), item, true)) - : null; - } - - bool HasMethod(T obj, string methodName) + private IEnumerable FilterDiagnosticsBySeverity(IEnumerable diagnostics) { - var type = obj.GetType(); - return type.GetMethod(methodName) != null; + return severity == null ? diagnostics : diagnostics.Where(IsDiagnosticSeverityAllowed); } - bool HasGetSeverity(T obj) + private static bool IsRuleEnabled(IRule rule) { - return HasMethod(obj, "GetSeverity"); + var configurableRule = rule as ConfigurableRule; + return configurableRule == null || configurableRule.Enable; } bool IsRuleAllowed(IRule rule) { - IEnumerable allowedSeverities = GetAllowedSeveritiesInInt(); bool includeRegexMatch = false; bool excludeRegexMatch = false; foreach (Regex include in includeRegexList) @@ -1981,12 +1965,8 @@ bool IsRuleAllowed(IRule rule) } } - bool helpRule = String.Equals(rule.GetName(), "PSUseUTF8EncodingForHelpFile", StringComparison.OrdinalIgnoreCase); - bool includeSeverity = IsSeverityAllowed(allowedSeverities, rule); - return (includeRule == null || includeRegexMatch) && (excludeRule == null || !excludeRegexMatch) - && IsSeverityAllowed(allowedSeverities, rule) && IsRuleEnabled(rule); } @@ -2358,13 +2338,14 @@ public IEnumerable AnalyzeSyntaxTree( // Need to reverse the concurrentbag to ensure that results are sorted in the increasing order of line numbers IEnumerable diagnosticsList = diagnostics.Reverse(); - return _suppressionPreference switch + IEnumerable selectedDiagnostics = _suppressionPreference switch { SuppressionPreference.SuppressedOnly => suppressed.OfType(), SuppressionPreference.Omit => diagnosticsList, SuppressionPreference.Include => diagnosticsList.Concat(suppressed.OfType()), _ => throw new ArgumentException($"SuppressionPreference has invalid value '{_suppressionPreference}'"), }; + return FilterDiagnosticsBySeverity(selectedDiagnostics); } } } diff --git a/Tests/Engine/CustomizedRule.tests.ps1 b/Tests/Engine/CustomizedRule.tests.ps1 index ce60f6dce..dd6230069 100644 --- a/Tests/Engine/CustomizedRule.tests.ps1 +++ b/Tests/Engine/CustomizedRule.tests.ps1 @@ -157,6 +157,30 @@ Describe "Test importing correct customized rules" { $customizedRulePath.Count | Should -Be 0 } + It "filters custom rule diagnostics by diagnostic severity" { + $customRuleContent = @' +function ReportError { + param ( + [System.Management.Automation.Language.CommandAst]$ast + ) + + if ($ast.GetCommandName() -eq 'Invoke-Something') { + New-Object -Typename 'Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord' ` + -ArgumentList 'This is an error', $ast.Extent, 'ReportError', Error, $ast.Extent.File, $null, $null + } +} +'@ + $customRuleScriptPath = Join-Path $TestDrive 'SeverityRule.psm1' + Set-Content -Path $customRuleScriptPath -Value $customRuleContent + + $errors = Invoke-ScriptAnalyzer -ScriptDefinition 'Invoke-Something' -CustomRulePath $customRuleScriptPath -Severity Error + $errors.Count | Should -Be 1 + $errors[0].Severity | Should -Be 'Error' + + $warnings = Invoke-ScriptAnalyzer -ScriptDefinition 'Invoke-Something' -CustomRulePath $customRuleScriptPath -Severity Warning + $warnings.Count | Should -Be 0 + } + It "can suppress custom rule with rule name expression ''" -TestCases @( @{RuleNameExpression = '$MyInvocation.MyCommand.Name'; RuleName = 'WarningAboutDoSomething' } @{RuleNameExpression = '$MyInvocation.InvocationName'; RuleName = 'MyCustomRule\WarningAboutDoSomething' } From 28e40a18bc6c70ee8b59e7ec31b12039c6e1eb9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:09:50 +0000 Subject: [PATCH 3/3] Test severity filtering of suppressed custom diagnostics Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Tests/Engine/CustomizedRule.tests.ps1 | 32 ++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/Tests/Engine/CustomizedRule.tests.ps1 b/Tests/Engine/CustomizedRule.tests.ps1 index dd6230069..a38275f97 100644 --- a/Tests/Engine/CustomizedRule.tests.ps1 +++ b/Tests/Engine/CustomizedRule.tests.ps1 @@ -166,12 +166,13 @@ function ReportError { if ($ast.GetCommandName() -eq 'Invoke-Something') { New-Object -Typename 'Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord' ` - -ArgumentList 'This is an error', $ast.Extent, 'ReportError', Error, $ast.Extent.File, $null, $null + -ArgumentList 'This is an error', $ast.Extent, 'ReportError', Error, $ast.Extent.File, 'custom-id', $null } } '@ $customRuleScriptPath = Join-Path $TestDrive 'SeverityRule.psm1' Set-Content -Path $customRuleScriptPath -Value $customRuleContent + (Get-ScriptAnalyzerRule -CustomRulePath $customRuleScriptPath).Severity | Should -Be 'Warning' $errors = Invoke-ScriptAnalyzer -ScriptDefinition 'Invoke-Something' -CustomRulePath $customRuleScriptPath -Severity Error $errors.Count | Should -Be 1 @@ -179,6 +180,35 @@ function ReportError { $warnings = Invoke-ScriptAnalyzer -ScriptDefinition 'Invoke-Something' -CustomRulePath $customRuleScriptPath -Severity Warning $warnings.Count | Should -Be 0 + + $script = @' +function Suppressed { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('ReportError', '', Justification = 'test justification')] + param() + Invoke-Something +} +Invoke-Something +'@ + foreach ($mode in 'SuppressedOnly', 'IncludeSuppressed') { + $parameters = @{ + ScriptDefinition = $script + CustomRulePath = $customRuleScriptPath + ErrorAction = 'Stop' + $mode = $true + } + + $errors = @(Invoke-ScriptAnalyzer @parameters -Severity Error) + $errors.Count | Should -Be $(if ($mode -eq 'SuppressedOnly') { 1 } else { 2 }) + $suppressed = @($errors | Where-Object IsSuppressed) + $suppressed.Count | Should -Be 1 + $suppressed[0].Severity | Should -Be 'Error' + $suppressed[0].RuleSuppressionID | Should -BeExactly 'custom-id' + $suppressed[0].Suppression | Should -HaveCount 1 + $suppressed[0].Suppression[0].Justification | Should -BeExactly 'test justification' + + $warnings = @(Invoke-ScriptAnalyzer @parameters -Severity Warning) + $warnings.Count | Should -Be 0 + } } It "can suppress custom rule with rule name expression ''" -TestCases @(