From 2aca47458d8c79fb055ba0776df5e128c6950634 Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Tue, 23 Jun 2026 10:35:48 -0500 Subject: [PATCH 1/4] Update example based on UUF comments --- .../PSScriptAnalyzer/create-custom-rule.md | 138 ++++++++---------- 1 file changed, 62 insertions(+), 76 deletions(-) diff --git a/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md b/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md index 748d200b..f3f60f92 100644 --- a/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md +++ b/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md @@ -33,7 +33,7 @@ Include the `.DESCRIPTION` field. This field becomes the description for the cus #> ``` -### Output type should be **DiagnosticRecord** +### Output type should be an array of **DiagnosticRecord** objects ```powershell [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] @@ -41,7 +41,7 @@ Include the `.DESCRIPTION` field. This field becomes the description for the cus ### Each function must have a Token array or an Ast parameter -The name of the **Ast** parameter name must end with **Ast**. +The name of the **Ast** parameter name must end with `Ast`. ```powershell Param @@ -53,7 +53,7 @@ Param ) ``` -The name of the **Token** parameter name must end with **Token**. +The name of the **Token** parameter name must end with `Token`. ```powershell Param @@ -108,7 +108,6 @@ $suggestedCorrections.add($correctionExtent) | Out-Null "Extent" = $ast.Extent "RuleName" = $PSCmdlet.MyInvocation.InvocationName "Severity" = "Warning" - "Severity" = "Warning" "RuleSuppressionID" = "MyRuleSuppressionID" "SuggestedCorrections" = $suggestedCorrections } @@ -132,7 +131,7 @@ Export-ModuleMember -Function (FunctionName) The #Requires statement prevents a script from running unless the Windows PowerShell version, modules, snap-ins, and module and snap-in version prerequisites are met. From Windows PowerShell 4.0, the #Requires statement let script developers require that - sessions be run with elevated user rights (run as Administrator). Script developers does + sessions be run with elevated user rights (run as Administrator). Script developers do not need to write their own methods any more. To fix a violation of this rule, please consider using #Requires -RunAsAdministrator instead of your own methods. .EXAMPLE @@ -144,11 +143,10 @@ Export-ModuleMember -Function (FunctionName) .NOTES None #> -function Measure-RequiresRunAsAdministrator -{ +function Measure-RequiresRunAsAdministrator { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] - Param + param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] @@ -156,83 +154,71 @@ function Measure-RequiresRunAsAdministrator $ScriptBlockAst ) - Process - { - $results = @() - try - { - #region Define predicates to find ASTs. - # Finds specific method, IsInRole. - [ScriptBlock]$predicate1 = { - param ([System.Management.Automation.Language.Ast]$Ast) - [bool]$returnValue = $false - if ($Ast -is [System.Management.Automation.Language.MemberExpressionAst]) - { - [System.Management.Automation.Language.MemberExpressionAst]$meAst = $Ast - if ($meAst.Member -is [System.Management.Automation.Language.StringConstantExpressionAst]) - { - [System.Management.Automation.Language.StringConstantExpressionAst]$sceAst = $meAst.Member - if ($sceAst.Value -eq 'isinrole') - { - $returnValue = $true - } - } - } - return $returnValue - } - - # Finds specific value, [system.security.principal.windowsbuiltinrole]::administrator. - [ScriptBlock]$predicate2 = { - param ([System.Management.Automation.Language.Ast]$Ast) - [bool]$returnValue = $false - if ($Ast -is [System.Management.Automation.Language.AssignmentStatementAst]) - { - [System.Management.Automation.Language.AssignmentStatementAst]$asAst = $Ast - if ($asAst.Right.ToString() -eq '[system.security.principal.windowsbuiltinrole]::administrator') - { + begin { + $MeasureRequiresAdmin = @( + 'The #Requires statement prevents a script from running unless the PowerShell version,' + 'modules, snap-ins, and module and snap-in version prerequisites are met. Since' + 'Windows PowerShell 4.0, the #Requires statement lets script developers require that' + 'sessions be run with elevated user rights (run as Administrator). Script developers' + 'don''t need to write their own methods to test for elevated rights. To fix a violation' + 'of this rule, use #Requires -RunAsAdministrator instead of your own methods.' + ) -join ' ' + + # Finds specific method, IsInRole. + [ScriptBlock]$predicate1 = { + param ([System.Management.Automation.Language.Ast]$Ast) + [bool]$returnValue = $false + if ($Ast -is [System.Management.Automation.Language.MemberExpressionAst]) { + [System.Management.Automation.Language.MemberExpressionAst]$meAst = $Ast + if ($meAst.Member -is [System.Management.Automation.Language.StringConstantExpressionAst]) { + [System.Management.Automation.Language.StringConstantExpressionAst]$sceAst = $meAst.Member + if ($sceAst.Value -eq 'IsInRole') { $returnValue = $true } } - return $returnValue } - #endregion - #region Finds ASTs that match the predicates. - - [System.Management.Automation.Language.Ast[]]$methodAst = $ScriptBlockAst.FindAll($predicate1, $true) - [System.Management.Automation.Language.Ast[]]$assignmentAst = $ScriptBlockAst.FindAll($predicate2, $true) - if ($null -ne $ScriptBlockAst.ScriptRequirements) - { - if ((!$ScriptBlockAst.ScriptRequirements.IsElevationRequired) -and - ($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0)) - { - $result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ - 'Message' = $Messages.MeasureRequiresRunAsAdministrator - 'Extent' = $assignmentAst.Extent - 'RuleName' = $PSCmdlet.MyInvocation.InvocationName - 'Severity' = 'Information' - } - $results += $result + return $returnValue + } + + # Finds specific value, [System.Security.Principal.WindowsBuiltInRole]::Administrator. + [ScriptBlock]$predicate2 = { + param ([System.Management.Automation.Language.Ast]$Ast) + [bool]$returnValue = $false + if ($Ast -is [System.Management.Automation.Language.AssignmentStatementAst]) { + [System.Management.Automation.Language.AssignmentStatementAst]$asAst = $Ast + if ($asAst.Right.ToString() -eq '[System.Security.Principal.WindowsBuiltInRole]::Administrator') { + $returnValue = $true } } - else - { - if (($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0)) - { - $result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ - 'Message' = $Messages.MeasureRequiresRunAsAdministrator - 'Extent' = $assignmentAst.Extent - 'RuleName' = $PSCmdlet.MyInvocation.InvocationName - 'Severity' = 'Information' - } - $results += $result + return $returnValue + } + } + + process { + # Find ASTs that match the predicates. + [System.Management.Automation.Language.Ast[]]$methodAst = $ScriptBlockAst.FindAll($predicate1, $true) + [System.Management.Automation.Language.Ast[]]$assignmentAst = $ScriptBlockAst.FindAll($predicate2, $true) + + if ($null -ne $ScriptBlockAst.ScriptRequirements) { + if ((!$ScriptBlockAst.ScriptRequirements.IsElevationRequired) -and + ($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0)) { + [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ + 'Message' = $MeasureRequiresAdmin + 'Extent' = $assignmentAst.Extent + 'RuleName' = $PSCmdlet.MyInvocation.InvocationName + 'Severity' = 'Information' } } - return $results - #endregion + return } - catch - { - $PSCmdlet.ThrowTerminatingError($PSItem) + + if (($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0)) { + [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ + 'Message' = $MeasureRequiresAdmin + 'Extent' = $assignmentAst.Extent + 'RuleName' = $PSCmdlet.MyInvocation.InvocationName + 'Severity' = 'Information' + } } } } From be1c85e00eb4ba4139eda4addab40faa505b59f7 Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Tue, 23 Jun 2026 11:12:00 -0500 Subject: [PATCH 2/4] Simplify example --- .../PSScriptAnalyzer/create-custom-rule.md | 93 ++++++------------- 1 file changed, 30 insertions(+), 63 deletions(-) diff --git a/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md b/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md index f3f60f92..1e2a21c1 100644 --- a/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md +++ b/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md @@ -76,10 +76,10 @@ The **DiagnosticRecord** should have at least four properties: ```powershell $result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{ - "Message" = "This is a sample rule" - "Extent" = $ast.Extent - "RuleName" = $PSCmdlet.MyInvocation.InvocationName - "Severity" = "Warning" + Message = 'This is a sample rule' + Extent = $ast.Extent + RuleName = $PSCmdlet.MyInvocation.InvocationName + Severity = 'Warning' } ``` @@ -87,13 +87,13 @@ Since version 1.17.0, you can include a **SuggestedCorrections** property of typ **IEnumerable\**. Make sure to specify the correct type. For example: ```powershell -[int]$startLineNumber = $ast.Extent.StartLineNumber -[int]$endLineNumber = $ast.Extent.EndLineNumber -[int]$startColumnNumber = $ast.Extent.StartColumnNumber -[int]$endColumnNumber = $ast.Extent.EndColumnNumber -[string]$correction = 'Correct text that replaces Extent text' -[string]$file = $MyInvocation.MyCommand.Definition -[string]$optionalDescription = 'Useful but optional description text' +$startLineNumber = $ast.Extent.StartLineNumber +$endLineNumber = $ast.Extent.EndLineNumber +$startColumnNumber = $ast.Extent.StartColumnNumber +$endColumnNumber = $ast.Extent.EndColumnNumber +$correction = 'Correct text that replaces Extent text' +$file = $MyInvocation.MyCommand.Definition +$optionalDescription = 'Useful but optional description text' $objParams = @{ TypeName = 'Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent' ArgumentList = $startLineNumber, $endLineNumber, $startColumnNumber, @@ -104,12 +104,12 @@ $suggestedCorrections = New-Object System.Collections.ObjectModel.Collection[$($ $suggestedCorrections.add($correctionExtent) | Out-Null [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ - "Message" = "This is a rule with a suggested correction" - "Extent" = $ast.Extent - "RuleName" = $PSCmdlet.MyInvocation.InvocationName - "Severity" = "Warning" - "RuleSuppressionID" = "MyRuleSuppressionID" - "SuggestedCorrections" = $suggestedCorrections + Message = 'This is a rule with a suggested correction' + Extent = $ast.Extent + RuleName = $PSCmdlet.MyInvocation.InvocationName + Severity = 'Warning' + RuleSuppressionID = 'MyRuleSuppressionID' + SuggestedCorrections = $suggestedCorrections } ``` @@ -165,59 +165,26 @@ function Measure-RequiresRunAsAdministrator { ) -join ' ' # Finds specific method, IsInRole. - [ScriptBlock]$predicate1 = { - param ([System.Management.Automation.Language.Ast]$Ast) - [bool]$returnValue = $false - if ($Ast -is [System.Management.Automation.Language.MemberExpressionAst]) { - [System.Management.Automation.Language.MemberExpressionAst]$meAst = $Ast - if ($meAst.Member -is [System.Management.Automation.Language.StringConstantExpressionAst]) { - [System.Management.Automation.Language.StringConstantExpressionAst]$sceAst = $meAst.Member - if ($sceAst.Value -eq 'IsInRole') { - $returnValue = $true - } - } - } - return $returnValue - } - - # Finds specific value, [System.Security.Principal.WindowsBuiltInRole]::Administrator. - [ScriptBlock]$predicate2 = { - param ([System.Management.Automation.Language.Ast]$Ast) - [bool]$returnValue = $false - if ($Ast -is [System.Management.Automation.Language.AssignmentStatementAst]) { - [System.Management.Automation.Language.AssignmentStatementAst]$asAst = $Ast - if ($asAst.Right.ToString() -eq '[System.Security.Principal.WindowsBuiltInRole]::Administrator') { - $returnValue = $true - } - } - return $returnValue + [ScriptBlock]$predicate = { + param($Ast) + return $Ast.Member.Value -eq 'IsInRole' } } process { - # Find ASTs that match the predicates. - [System.Management.Automation.Language.Ast[]]$methodAst = $ScriptBlockAst.FindAll($predicate1, $true) - [System.Management.Automation.Language.Ast[]]$assignmentAst = $ScriptBlockAst.FindAll($predicate2, $true) - - if ($null -ne $ScriptBlockAst.ScriptRequirements) { - if ((!$ScriptBlockAst.ScriptRequirements.IsElevationRequired) -and - ($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0)) { - [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ - 'Message' = $MeasureRequiresAdmin - 'Extent' = $assignmentAst.Extent - 'RuleName' = $PSCmdlet.MyInvocation.InvocationName - 'Severity' = 'Information' - } - } - return + # Exit early if the script block has a #Requires -RunAsAdministrator statement. + if ($ScriptBlockAst.ScriptRequirements.IsElevationRequired) { + return } - if (($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0)) { + # Test for calls to IsInRole() method + [System.Management.Automation.Language.Ast]$methodAst = $ScriptBlockAst.Find($predicate, $true) + if ($methodAst) { [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ - 'Message' = $MeasureRequiresAdmin - 'Extent' = $assignmentAst.Extent - 'RuleName' = $PSCmdlet.MyInvocation.InvocationName - 'Severity' = 'Information' + Message = $MeasureRequiresAdmin + Extent = $methodAst.Extent + RuleName = $PSCmdlet.MyInvocation.InvocationName + Severity = 'Information' } } } From f854fccdae9b56f06c6eea731de026114d0f8a5b Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Tue, 23 Jun 2026 14:53:03 -0500 Subject: [PATCH 3/4] Add reason for AvoidGlobalVars --- .../PSScriptAnalyzer/Rules/AvoidGlobalVars.md | 29 +++++++++++-------- .../Rules/UseApprovedVerbs.md | 6 ++-- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidGlobalVars.md b/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidGlobalVars.md index add12c62..74e10d7d 100644 --- a/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidGlobalVars.md +++ b/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidGlobalVars.md @@ -1,6 +1,6 @@ --- description: Avoid global variables -ms.date: 05/28/2026 +ms.date: 06/23/2026 ms.topic: reference title: AvoidGlobalVars --- @@ -10,11 +10,14 @@ title: AvoidGlobalVars ## Description +You should avoid modifying global variables in your scripts and functions because other scripts or +function that run in the same session can depend of them. This can lead to unexpected behavior and +make it difficult to debug your code. + This rule detects usage of variables with the global scope modifier. PowerShell controls access to variables, functions, aliases, and drives through a mechanism known as scoping. Variables and -functions that are present when PowerShell starts have been created in the global scope. - -Globally scoped variables include: +functions that are present when PowerShell starts were created in the global scope. Globally scoped +variables include: - Automatic variables - Preference variables @@ -34,21 +37,23 @@ Use local or script scope for variables instead of the global scope. To learn mo ### Noncompliant ```powershell -$Global:var1 = $null -function Test-NotGlobal ($var) -{ - $a = $var + $var1 +$var1 = 'foo' +function Test-NotGlobal ($var) { + $Global:var1 = $var } +Test-NotGlobal 'bar' +$var1 ``` ### Compliant ```powershell -$var1 = $null -function Test-NotGlobal ($var1, $var2) -{ - $a = $var1 + $var2 +$var1 = "foo" +function Test-NotGlobal ($var) { + $var1 = $var } +Test-NotGlobal 'bar' +$var1 ``` diff --git a/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseApprovedVerbs.md b/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseApprovedVerbs.md index 436db23c..0223627d 100644 --- a/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseApprovedVerbs.md +++ b/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseApprovedVerbs.md @@ -25,8 +25,7 @@ To learn more, see [Approved Verbs for PowerShell Commands][01]. ### Noncompliant ```powershell -function Change-Item -{ +function Change-Item { ... } ``` @@ -34,8 +33,7 @@ function Change-Item ### Compliant ```powershell -function Update-Item -{ +function Update-Item { ... } ``` From e1a0027115a9808326f3397cf9ca5e4024c619fe Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Tue, 23 Jun 2026 15:20:25 -0500 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../docs-conceptual/PSScriptAnalyzer/Rules/AvoidGlobalVars.md | 4 ++-- .../docs-conceptual/PSScriptAnalyzer/create-custom-rule.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidGlobalVars.md b/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidGlobalVars.md index 74e10d7d..ba8413eb 100644 --- a/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidGlobalVars.md +++ b/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidGlobalVars.md @@ -11,7 +11,7 @@ title: AvoidGlobalVars ## Description You should avoid modifying global variables in your scripts and functions because other scripts or -function that run in the same session can depend of them. This can lead to unexpected behavior and +functions that run in the same session can depend on them. This can lead to unexpected behavior and make it difficult to debug your code. This rule detects usage of variables with the global scope modifier. PowerShell controls access to @@ -48,7 +48,7 @@ $var1 ### Compliant ```powershell -$var1 = "foo" +$var1 = 'foo' function Test-NotGlobal ($var) { $var1 = $var } diff --git a/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md b/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md index 1e2a21c1..c4a3feaf 100644 --- a/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md +++ b/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md @@ -130,7 +130,7 @@ Export-ModuleMember -Function (FunctionName) .DESCRIPTION The #Requires statement prevents a script from running unless the Windows PowerShell version, modules, snap-ins, and module and snap-in version prerequisites are met. - From Windows PowerShell 4.0, the #Requires statement let script developers require that + Since Windows PowerShell 4.0, the #Requires statement lets script developers require that sessions be run with elevated user rights (run as Administrator). Script developers do not need to write their own methods any more. To fix a violation of this rule, please consider using #Requires -RunAsAdministrator instead of your own methods.