Skip to content

Commit 67ffb24

Browse files
Fix SuppressMessage CustomRule (#2142)
* Fix RuleSuppressionID not working with named arguments Problem: SuppressMessageAttribute failed when using named arguments for RuleSuppressionID. Users could not use syntax like: [SuppressMessage("RuleName", RuleSuppressionId="MyId")] Root Cause: In RuleSuppression.cs, the named argument parser had two bugs: 1. Checked if RuleName was set instead of RuleSuppressionID 2. Assigned the value to RuleName instead of RuleSuppressionID This broke selective rule suppression for custom rules. Solution: - Fixed conflict check to validate RuleSuppressionID instead of RuleName - Fixed assignment to set RuleSuppressionID instead of RuleName - Added comprehensive tests for named argument syntax - Minor formatting improvements Now both syntaxes work correctly: [SuppressMessage("Rule", RuleSuppressionId="Id", Scope="Function")] [SuppressMessage("Rule", "Id", Scope="Function")] * test: ✨ Add test for custom rule suppression with RuleSuppressionID * Implemented a test case to validate the functionality of custom rules with targeted suppression. * The test recreates the scenario from GitHub issue #1686, ensuring that `RuleSuppressionID` works correctly with named arguments. * Verified that violations are suppressed as expected based on the defined rules.
1 parent 4ad5980 commit 67ffb24

File tree

2 files changed

+178
-21
lines changed

2 files changed

+178
-21
lines changed

Engine/Generic/RuleSuppression.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,12 @@ public RuleSuppression(AttributeAst attrAst, int start, int end)
193193
}
194194
else if (argumentName.Equals("rulesuppressionid", StringComparison.OrdinalIgnoreCase))
195195
{
196-
if (!String.IsNullOrWhiteSpace(RuleName))
196+
if (!String.IsNullOrWhiteSpace(RuleSuppressionID))
197197
{
198198
Error = String.Format(Strings.NamedAndPositionalArgumentsConflictError, name);
199199
}
200200

201-
RuleName = (name.Argument as StringConstantExpressionAst).Value;
201+
RuleSuppressionID = (name.Argument as StringConstantExpressionAst).Value;
202202
}
203203
else if (argumentName.Equals("scope", StringComparison.OrdinalIgnoreCase))
204204
{
@@ -333,12 +333,12 @@ public static List<RuleSuppression> GetSuppressions(IEnumerable<AttributeAst> at
333333
{
334334
targetAsts = scopeAst.FindAll(ast => ast is FunctionDefinitionAst && reg.IsMatch((ast as FunctionDefinitionAst).Name), true);
335335
}
336-
#if !(PSV3 || PSV4)
336+
#if !(PSV3 || PSV4)
337337
else if (scope.Equals("class", StringComparison.OrdinalIgnoreCase))
338338
{
339339
targetAsts = scopeAst.FindAll(ast => ast is TypeDefinitionAst && reg.IsMatch((ast as TypeDefinitionAst).Name), true);
340340
}
341-
#endif
341+
#endif
342342

343343
if (targetAsts != null)
344344
{

Tests/Engine/RuleSuppression.tests.ps1

Lines changed: 174 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -56,28 +56,28 @@ Describe "RuleSuppressionWithoutScope" {
5656

5757
It "Suppresses rule with extent created using ScriptExtent constructor" {
5858
Invoke-ScriptAnalyzer `
59-
-ScriptDefinition $ruleSuppressionAvoidUsernameAndPassword `
60-
-IncludeRule "PSAvoidUsingUserNameAndPassWordParams" `
61-
-OutVariable ruleViolations `
62-
-SuppressedOnly
59+
-ScriptDefinition $ruleSuppressionAvoidUsernameAndPassword `
60+
-IncludeRule "PSAvoidUsingUserNameAndPassWordParams" `
61+
-OutVariable ruleViolations `
62+
-SuppressedOnly
6363
$ruleViolations.Count | Should -Be 1
64-
}
64+
}
6565
}
6666

6767
Context "Script" {
6868
It "Does not raise violations" {
69-
$suppression = $violations | Where-Object {$_.RuleName -eq "PSProvideCommentHelp" }
69+
$suppression = $violations | Where-Object { $_.RuleName -eq "PSProvideCommentHelp" }
7070
$suppression.Count | Should -Be 0
71-
$suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSProvideCommentHelp" }
71+
$suppression = $violationsUsingScriptDefinition | Where-Object { $_.RuleName -eq "PSProvideCommentHelp" }
7272
$suppression.Count | Should -Be 0
7373
}
7474
}
7575

7676
Context "RuleSuppressionID" {
7777
It "Only suppress violations for that ID" {
78-
$suppression = $violations | Where-Object {$_.RuleName -eq "PSAvoidDefaultValueForMandatoryParameter" }
78+
$suppression = $violations | Where-Object { $_.RuleName -eq "PSAvoidDefaultValueForMandatoryParameter" }
7979
$suppression.Count | Should -Be 1
80-
$suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSAvoidDefaultValueForMandatoryParameter" }
80+
$suppression = $violationsUsingScriptDefinition | Where-Object { $_.RuleName -eq "PSAvoidDefaultValueForMandatoryParameter" }
8181
$suppression.Count | Should -Be 1
8282
}
8383

@@ -93,10 +93,10 @@ function SuppressPwdParam()
9393
}
9494
'@
9595
Invoke-ScriptAnalyzer `
96-
-ScriptDefinition $ruleSuppressionIdAvoidPlainTextPassword `
97-
-IncludeRule "PSAvoidUsingPlainTextForPassword" `
98-
-OutVariable ruleViolations `
99-
-SuppressedOnly
96+
-ScriptDefinition $ruleSuppressionIdAvoidPlainTextPassword `
97+
-IncludeRule "PSAvoidUsingPlainTextForPassword" `
98+
-OutVariable ruleViolations `
99+
-SuppressedOnly
100100
$ruleViolations.Count | Should -Be 1
101101
}
102102

@@ -246,8 +246,165 @@ function MyFunc
246246
}
247247
}
248248

249+
Context "RuleSuppressionID with named arguments" {
250+
It "Should work with named argument syntax" {
251+
$scriptWithNamedArgs = @'
252+
function SuppressPasswordParam()
253+
{
254+
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(RuleName="PSAvoidUsingPlainTextForPassword", RuleSuppressionId="password1")]
255+
param(
256+
[string] $password1,
257+
[string] $password2
258+
)
259+
}
260+
'@
261+
262+
$diagnostics = Invoke-ScriptAnalyzer `
263+
-ScriptDefinition $scriptWithNamedArgs `
264+
-IncludeRule "PSAvoidUsingPlainTextForPassword"
265+
$suppressions = Invoke-ScriptAnalyzer `
266+
-ScriptDefinition $scriptWithNamedArgs `
267+
-IncludeRule "PSAvoidUsingPlainTextForPassword" `
268+
-SuppressedOnly
269+
270+
# There should be one unsuppressed diagnostic (password2) and one suppressed diagnostic (password1)
271+
$diagnostics | Should -HaveCount 1
272+
$diagnostics[0].RuleName | Should -BeExactly "PSAvoidUsingPlainTextForPassword"
273+
$diagnostics[0].RuleSuppressionID | Should -BeExactly "password2"
274+
275+
$suppressions | Should -HaveCount 1
276+
$suppressions[0].RuleName | Should -BeExactly "PSAvoidUsingPlainTextForPassword"
277+
$suppressions[0].RuleSuppressionID | Should -BeExactly "password1"
278+
}
279+
280+
It "Should work with mixed positional and named argument syntax" {
281+
$scriptWithMixedArgs = @'
282+
function SuppressPasswordParam()
283+
{
284+
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", Scope="Function")]
285+
param(
286+
[string] $password1,
287+
[string] $password2
288+
)
289+
}
290+
'@
291+
292+
$diagnostics = Invoke-ScriptAnalyzer `
293+
-ScriptDefinition $scriptWithMixedArgs `
294+
-IncludeRule "PSAvoidUsingPlainTextForPassword"
295+
296+
# All violations should be suppressed since there's no RuleSuppressionID filtering
297+
$diagnostics | Should -HaveCount 0
298+
}
299+
300+
It "Should work with custom rule from issue #1686 comment" {
301+
# This test recreates the exact scenario from GitHub issue 1686 comment
302+
# with a custom rule that populates RuleSuppressionID for targeted suppression
303+
304+
# Custom rule module that creates violations with specific RuleSuppressionIDs
305+
$customRuleScript = @'
306+
function Measure-AvoidFooBarCommand {
307+
[CmdletBinding()]
308+
[OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
309+
param(
310+
[Parameter(Mandatory)]
311+
[ValidateNotNullOrEmpty()]
312+
[System.Management.Automation.Language.ScriptBlockAst]
313+
$ScriptBlockAst
314+
)
315+
316+
$results = @()
317+
318+
# Find all command expressions
319+
$commandAsts = $ScriptBlockAst.FindAll({
320+
param($node)
321+
$node -is [System.Management.Automation.Language.CommandAst]
322+
}, $true)
323+
324+
foreach ($commandAst in $commandAsts) {
325+
$commandName = $commandAst.GetCommandName()
326+
if ($commandName -match '^(Get-FooBar|Set-FooBar)$') {
327+
# Create a diagnostic with the command name as RuleSuppressionID
328+
$result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]::new(
329+
"Avoid using $commandName command",
330+
$commandAst.Extent,
331+
'Measure-AvoidFooBarCommand',
332+
'Warning',
333+
$null,
334+
$commandName # This becomes the RuleSuppressionID
335+
)
336+
$results += $result
337+
}
338+
}
339+
340+
return $results
341+
}
342+
343+
Export-ModuleMember -Function Measure-AvoidFooBarCommand
344+
'@
345+
346+
# Script that uses the custom rule with targeted suppression
347+
$scriptWithCustomRuleSuppression = @'
348+
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('Measure-AvoidFooBarCommand', RuleSuppressionId = 'Get-FooBar', Scope = 'Function', Target = 'Allow-GetFooBar')]
349+
param()
350+
351+
function Test-BadCommands {
352+
Get-FooBar # Line 6 - Should NOT be suppressed (wrong function)
353+
Set-FooBar # Line 7 - Should NOT be suppressed (different RuleSuppressionID)
354+
}
355+
356+
function Allow-GetFooBar {
357+
Get-FooBar # Line 11 - Should be suppressed (matches RuleSuppressionId and Target)
358+
Set-FooBar # Line 12 - Should NOT be suppressed (different RuleSuppressionID)
359+
}
360+
'@
361+
362+
# Save custom rule to temporary file
363+
$customRuleFile = [System.IO.Path]::GetTempFileName()
364+
$customRuleModuleFile = [System.IO.Path]::ChangeExtension($customRuleFile, '.psm1')
365+
Set-Content -Path $customRuleModuleFile -Value $customRuleScript
366+
367+
try
368+
{
369+
# Check suppressed violations - this is the key test for our fix
370+
$suppressions = Invoke-ScriptAnalyzer `
371+
-ScriptDefinition $scriptWithCustomRuleSuppression `
372+
-CustomRulePath $customRuleModuleFile `
373+
-SuppressedOnly `
374+
-ErrorAction SilentlyContinue
375+
376+
# The core functionality: RuleSuppressionID with named arguments should work for custom rules
377+
# We should have at least one suppressed Get-FooBar violation
378+
$suppressions | Should -Not -BeNullOrEmpty -Because "RuleSuppressionID named arguments should work for custom rules"
379+
380+
$getFooBarSuppressions = $suppressions | Where-Object { $_.RuleSuppressionID -eq 'Get-FooBar' }
381+
$getFooBarSuppressions | Should -Not -BeNullOrEmpty -Because "Get-FooBar should be suppressed based on RuleSuppressionID"
382+
383+
# Verify the suppression occurred in the right function (Allow-GetFooBar)
384+
$getFooBarSuppressions | Should -Not -BeNullOrEmpty
385+
$getFooBarSuppressions[0].RuleName | Should -BeExactly 'Measure-AvoidFooBarCommand'
386+
387+
# Get unsuppressed violations to verify selective suppression
388+
$diagnostics = Invoke-ScriptAnalyzer `
389+
-ScriptDefinition $scriptWithCustomRuleSuppression `
390+
-CustomRulePath $customRuleModuleFile `
391+
-ErrorAction SilentlyContinue
392+
393+
# Should still have violations for Set-FooBar (different RuleSuppressionID) and Get-FooBar in wrong function
394+
$setFooBarViolations = $diagnostics | Where-Object { $_.RuleSuppressionID -eq 'Set-FooBar' }
395+
$setFooBarViolations | Should -Not -BeNullOrEmpty -Because "Set-FooBar should not be suppressed (different RuleSuppressionID)"
396+
397+
}
398+
finally
399+
{
400+
Remove-Item -Path $customRuleModuleFile -ErrorAction SilentlyContinue
401+
Remove-Item -Path $customRuleFile -ErrorAction SilentlyContinue
402+
}
403+
}
404+
}
405+
249406
Context "Rule suppression within DSC Configuration definition" {
250-
It "Suppresses rule" -skip:($IsLinux -or $IsMacOS -or ($PSVersionTable.PSVersion.Major -lt 5)) {
407+
It "Suppresses rule" -Skip:($IsLinux -or $IsMacOS -or ($PSVersionTable.PSVersion.Major -lt 5)) {
251408
$suppressedRule = Invoke-ScriptAnalyzer -ScriptDefinition $ruleSuppressionInConfiguration -SuppressedOnly
252409
$suppressedRule.Count | Should -Be 1
253410
}
@@ -281,9 +438,9 @@ function MyFunc
281438
Describe "RuleSuppressionWithScope" {
282439
Context "FunctionScope" {
283440
It "Does not raise violations" {
284-
$suppression = $violations | Where-Object {$_.RuleName -eq "PSAvoidUsingPositionalParameters" }
441+
$suppression = $violations | Where-Object { $_.RuleName -eq "PSAvoidUsingPositionalParameters" }
285442
$suppression.Count | Should -Be 0
286-
$suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSAvoidUsingPositionalParameters" }
443+
$suppression = $violationsUsingScriptDefinition | Where-Object { $_.RuleName -eq "PSAvoidUsingPositionalParameters" }
287444
$suppression.Count | Should -Be 0
288445
}
289446
}
@@ -353,4 +510,4 @@ Describe "RuleSuppressionWithScope" {
353510
$suppressed.Count | Should -Be 1
354511
}
355512
}
356-
}
513+
}

0 commit comments

Comments
 (0)