PSScriptAnalyzerCustomRules.psm1
function Measure-AdvancedFunctionHelp { <# .SYNOPSIS Named script blocks (Begin, Process, etc...) should be PascalCase. .DESCRIPTION The first letter of named script block names should be capitalized. This rule can auto-fix violations. .EXAMPLE # BAD function Get-Example { [CmdletBinding()] Param() 'Bad' | Write-Color -Red } # GOOD function Get-Example { < Help Content Goes Here > [CmdletBinding()] Param() 'Good' | Write-Color -Green } .INPUTS [System.Management.Automation.Language.ScriptBlockAst] .OUTPUTS [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]] .NOTES Reference: Common software standard #> [CmdletBinding()] [OutputType([Object[]])] Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst] $ScriptBlockAst ) Begin { $Results = @() $IsFunctionDefinition = { Param( [System.Management.Automation.Language.Ast] $Ast ) $Ast -is [System.Management.Automation.Language.FunctionDefinitionAst] } $HasCmdletBinding = { Param( [System.Management.Automation.Language.Ast] $Ast ) $Ast -is [System.Management.Automation.Language.AttributeAst] -and ($Ast.TypeName.Name -eq 'CmdletBinding') } } Process { try { $Functions = $ScriptBlockAst.FindAll($IsFunctionDefinition, $False) foreach ($Function in $Functions) { $IsAdvancedFunction = $Function.Find($HasCmdletBinding, $True) $Help = $Function.GetHelpContent() if (-not $Function.IsWorkflow -and $IsAdvancedFunction -and -not $Help.Synopsis) { $Results += [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{ Message = "$($Function.Name) should have help content" RuleName = 'AdvancedFunctionHelpContent' Severity = 'Information' Extent = $Function.Extent } } } return $Results } catch { $PSCmdlet.ThrowTerminatingError($PSItem) } } } function Measure-NamedBlockPascalCase { <# .SYNOPSIS Named script blocks (Begin, Process, etc...) should be PascalCase. .DESCRIPTION The first letter of named script block names should be capitalized. This rule can auto-fix violations. .EXAMPLE # BAD process {...} #GOOD Process {...} .INPUTS [System.Management.Automation.Language.ScriptBlockAst] .OUTPUTS [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]] .NOTES Reference: Personal preference Note: Whether you prefer title case named script blocks or otherwise, consistency is what matters most. #> [CmdletBinding()] [OutputType([Object[]])] Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst] $ScriptBlockAst ) Begin { $Results = @() $Predicate = { Param( [System.Management.Automation.Language.Ast] $Ast ) ($Ast -is [System.Management.Automation.Language.NamedBlockAst]) -and -not $Ast.Unnamed -and -not ($Ast.Extent.Text -cmatch '^[A-Z]') } } Process { try { $Violations = $ScriptBlockAst.FindAll($Predicate, $False) foreach ($Violation in $Violations) { $Extent = $Violation.Extent $Correction = $Extent.Text[0].ToString().ToUpper() + $Extent.Text.SubString(1) $CorrectionExtent = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent]::New( $Extent.StartLineNumber, $Extent.EndLineNumber, $Extent.StartColumnNumber, $Extent.EndColumnNumber, $Correction, ''# optional description - intentionally left blank ) $SuggestedCorrections = New-Object System.Collections.ObjectModel.Collection['Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent'] [Void]$SuggestedCorrections.Add($CorrectionExtent) $Results += [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{ Message = 'Named script block names should be PascalCase' RuleName = 'NamedBlockPascalCase' Severity = 'Warning' Extent = $Extent SuggestedCorrections = $SuggestedCorrections } } return $Results } catch { $PSCmdlet.ThrowTerminatingError($PSItem) } } } function Measure-OperatorLowerCase { <# .SYNOPSIS Operators (-join, -split, etc...) should be lowercase. .DESCRIPTION Operators should not be capitalized. This rule can auto-fix violations. .EXAMPLE # BAD $Foo -Join $Bar #GOOD $Foo -join $Bar .INPUTS [System.Management.Automation.Language.ScriptBlockAst] .OUTPUTS [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]] .NOTES Reference: Personal preference Note: Whether you prefer lowercase operators or otherwise, consistency is what matters most. #> [CmdletBinding()] [OutputType([Object[]])] Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst] $ScriptBlockAst ) Begin { $Results = @() $Predicate = { Param( [System.Management.Automation.Language.Ast] $Ast ) ($Ast -is [System.Management.Automation.Language.BinaryExpressionAst]) -and ($Ast.ErrorPosition.Text -cmatch '[A-Z]') } } Process { try { $Violations = $ScriptBlockAst.FindAll($Predicate, $False) foreach ($Violation in $Violations) { $Extent = $Violation.Extent $ErrorPosition = $Violation.ErrorPosition $StartColumnNumber = $Extent.StartColumnNumber $Start = $ErrorPosition.StartColumnNumber - $StartColumnNumber $End = $ErrorPosition.EndColumnNumber - $StartColumnNumber $Correction = $Extent.Text.SubString(0, $Start) + $ErrorPosition.Text.ToLower() + $Extent.Text.SubString($End) $CorrectionExtent = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent]::New( $Extent.StartLineNumber, $Extent.EndLineNumber, $StartColumnNumber, $Extent.EndColumnNumber, $Correction, ''# optional description - intentionally left blank ) $SuggestedCorrections = New-Object System.Collections.ObjectModel.Collection['Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent'] [Void]$SuggestedCorrections.Add($CorrectionExtent) $Results += [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{ Message = 'Operators should be lowercase' RuleName = 'OperatorLowerCase' Severity = 'Warning' Extent = $Extent SuggestedCorrections = $SuggestedCorrections } } return $Results } catch { $PSCmdlet.ThrowTerminatingError($PSItem) } } } function Measure-ParamPascalCaseNoTrailingSpace { <# .SYNOPSIS Param block keyword should be PascalCase. .DESCRIPTION The "p" of "param" should be capitalized. This rule can auto-fix violations. .EXAMPLE # BAD param() #GOOD Param() .INPUTS [System.Management.Automation.Language.ScriptBlockAst] .OUTPUTS [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]] .NOTES Reference: Personal preference Note: Whether you prefer PascalCase or otherwise, consistency is what matters most. #> [CmdletBinding()] [OutputType([Object[]])] Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst] $ScriptBlockAst ) Begin { $Results = @() $Predicate = { Param( [System.Management.Automation.Language.Ast] $Ast ) ($Ast -is [System.Management.Automation.Language.ParamBlockAst]) -and -not ($Ast.Extent.Text -cmatch 'Param\(') } } Process { try { $Violations = $ScriptBlockAst.FindAll($Predicate, $False) foreach ($Violation in $Violations) { $Extent = $Violation.Extent $Correction = $Extent.Text -replace '^param\s*\(','Param(' $CorrectionExtent = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent]::New( $Extent.StartLineNumber, $Extent.EndLineNumber, $Extent.StartColumnNumber, $Extent.EndColumnNumber, $Correction, ''# optional description - intentionally left blank ) $SuggestedCorrections = New-Object System.Collections.ObjectModel.Collection['Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent'] [Void]$SuggestedCorrections.Add($CorrectionExtent) $Results += [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{ Message = 'Param block keyword should be PascalCase with no trailing spaces' RuleName = 'ParamPascalCaseNoTrailingSpace' Severity = 'Warning' Extent = $Extent SuggestedCorrections = $SuggestedCorrections } } return $Results } catch { $PSCmdlet.ThrowTerminatingError($PSItem) } } } function Measure-TypeAttributePascalCase { <# .SYNOPSIS Type annotations ([String], [Array], etc...) should be PascalCase. .DESCRIPTION The first letter of type annotations should be capitalized. .EXAMPLE # BAD [bool]$Foo = $False #GOOD [Bool]$Foo = $True .INPUTS [System.Management.Automation.Language.ScriptBlockAst] .OUTPUTS [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]] .NOTES Reference: Personal preference Note: Whether you prefer PascalCase type names or otherwise, consistency is what matters most. #> [CmdletBinding()] [OutputType([Object[]])] Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst] $ScriptBlockAst ) Begin { $Results = @() $Predicate = { Param( [System.Management.Automation.Language.Ast] $Ast ) $IsApplicable = ($Ast -is [System.Management.Automation.Language.TypeExpressionAst]) -or ($Ast -is [System.Management.Automation.Language.TypeConstraintAst]) $Text = $Ast.Extent.Text -replace '[\[\]]','' $IsViolation = $IsApplicable -and -not ($Text -cmatch '^([A-Z]+[a-z0-9]+)((\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?') $IsViolation } } Process { try { $Violations = $ScriptBlockAst.FindAll($Predicate, $False) foreach ($Violation in $Violations) { $Results += [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{ Message = "Type attribute, `"$($Violation.Extent.Text)`", should be PascalCase" RuleName = 'TypeAttributePascalCase' Severity = 'Warning' Extent = $Violation.Extent } } return $Results } catch { $PSCmdlet.ThrowTerminatingError($PSItem) } } } function Measure-VariablePascalCase { <# .SYNOPSIS Variables ($Foo, $Bar, etc...) should be PascalCase. .DESCRIPTION The first letter of a variable names should be capitalized. .EXAMPLE # BAD $foo = 'foo' $bar = 'bar' #GOOD $Foo = 'foo' $Bar = 'bar' .INPUTS [System.Management.Automation.Language.ScriptBlockAst] .OUTPUTS [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]] .NOTES Reference: Personal preference Note: Whether you prefer PascalCase variable names or otherwise, consistency is what matters most. #> [CmdletBinding()] [OutputType([Object[]])] Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst] $ScriptBlockAst ) Begin { $Results = @() $Predicate = { Param( [System.Management.Automation.Language.Ast] $Ast ) $IsVariableExpression = $Ast -is [System.Management.Automation.Language.VariableExpressionAst] $Name = $Ast.Extent.Text -replace '[{}]','' $IsVariableExpression -and -not $Name.StartsWith('$_') -and ($Name -ne '$this') -and ($Name[1] -cnotmatch '[A-Z]') } } Process { try { $Violations = $ScriptBlockAst.FindAll($Predicate, $False) foreach ($Violation in $Violations) { $Results += [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{ Message = "Variable name, `"$($Violation.Extent.Text)`", should be PascalCase" RuleName = 'VariablePascalCase' Severity = 'Warning' Extent = $Violation.Extent } } return $Results } catch { $PSCmdlet.ThrowTerminatingError($PSItem) } } } function Measure-UseRequiresDirective { <# .SYNOPSIS "Requires" should be used instead of "Import-Module" .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 3.0, the #Requires statement let script developers specify Windows PowerShell modules that the script requires. .EXAMPLE #BAD Import-Module -Name SomeModule #GOOD (at top of file) #Requires -Modules SomeModule .INPUTS [System.Management.Automation.Language.ScriptBlockAst] .OUTPUTS [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]] .NOTES See https://github.com/PowerShell/PSScriptAnalyzer/blob/development/Tests/Engine/CommunityAnalyzerRules/CommunityAnalyzerRules.psm1 #> [CmdletBinding()] [OutputType([Object[]])] Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst] $ScriptBlockAst ) Begin { $Results = @() $RuleName = 'RequireDirective' $Message = '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. To fix a violation of this rule, please consider to use #Requires -RunAsAdministrator instead of using Import-Module' $Predicate = { Param( [System.Management.Automation.Language.Ast] $Ast ) ($Ast -is [System.Management.Automation.Language.CommandAst]) -and ($Null -ne $Ast.GetCommandName()) -and ($Ast.GetCommandName() -eq 'import-module') } } Process { try { $Violations = $ScriptBlockAst.FindAll($Predicate, $True) if ($Null -ne $ScriptBlockAst.ScriptRequirements) { if ($ScriptBlockAst.ScriptRequirements.RequiredModules.Count -eq 0) { foreach ($Violation in $Violations) { $Results += [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{ Message = $Message RuleName = $RuleName Severity = 'Information' Extent = $Violation.Extent } } } } else { foreach ($Violation in $Violations) { $Results += [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{ RuleName = $RuleName Message = $Message Severity = 'Information' Extent = $Violation.Extent } } } return $Results } catch { $PSCmdlet.ThrowTerminatingError($PSItem) } } } |