Indented.ScriptAnalyzerRules.psm1
using namespace System.Management.Automation using namespace System.Management.Automation.Language using namespace System.Reflection using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic using namespace System.Collections.Generic #Region '.\public\helper\Get-FunctionInfo.ps1' 0 #using namespace System.Management.Automation #using namespace System.Management.Automation.Language #using namespace System.Reflection function Get-FunctionInfo { <# .SYNOPSIS Get an instance of FunctionInfo. .DESCRIPTION FunctionInfo does not present a public constructor. This function calls an internal / private constructor on FunctionInfo to create a description of a function from a script block or file containing one or more functions. .INPUTS System.String .EXAMPLE Get-ChildItem -Filter *.psm1 | Get-FunctionInfo Get all functions declared within the *.psm1 file and construct FunctionInfo. .EXAMPLE Get-ChildItem C:\Scripts -Filter *.ps1 -Recurse | Get-FunctionInfo Get all functions declared in all ps1 files in C:\Scripts. #> [CmdletBinding(DefaultParameterSetName = 'FromPath')] [OutputType([System.Management.Automation.FunctionInfo])] param ( # The path to a file containing one or more functions. [Parameter(Position = 1, ValueFromPipelineByPropertyName, ParameterSetName = 'FromPath')] [Alias('FullName')] [string]$Path, # A script block containing one or more functions. [Parameter(ParameterSetName = 'FromScriptBlock')] [ScriptBlock]$ScriptBlock, # By default functions nested inside other functions are ignored. Setting this parameter will allow nested functions to be discovered. [Switch]$IncludeNested ) begin { $executionContextType = [PowerShell].Assembly.GetType('System.Management.Automation.ExecutionContext') $constructor = [FunctionInfo].GetConstructor( [BindingFlags]'NonPublic, Instance', $null, [CallingConventions]'Standard, HasThis', ([String], [ScriptBlock], $executionContextType), $null ) } process { if ($pscmdlet.ParameterSetName -eq 'FromPath') { try { $scriptBlock = [ScriptBlock]::Create((Get-Content $Path -Raw)) } catch { $ErrorRecord = @{ Exception = $_.Exception.InnerException ErrorId = 'InvalidScriptBlock' Category = 'OperationStopped' } Write-Error @ErrorRecord } } if ($scriptBlock) { $scriptBlock.Ast.FindAll( { param( $ast ) $ast -is [FunctionDefinitionAst] }, $IncludeNested ) | ForEach-Object { $constructor.Invoke(([String]$_.Name, $_.Body.GetScriptBlock(), $null)) } } } } #EndRegion '.\public\helper\Get-FunctionInfo.ps1' 81 #Region '.\public\helper\Invoke-CustomScriptAnalyzerRule.ps1' 0 function Invoke-CustomScriptAnalyzerRule { <# .SYNOPSIS Invoke a specific coding convention rule. .DESCRIPTION Invoke a specific coding convention rule against a defined file, script block, or command name. .EXAMPLE Invoke-CustomScriptAnalyzerRule -Path C:\Script.ps1 -RuleName AvoidNestedFunctions Invoke the rule AvoidNestedFunctions against the script in the specified path. #> [CmdletBinding(DefaultParameterSetName = 'FromPath')] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [Parameter(Mandatory, ParameterSetName = 'FromString')] [string]$String, [Parameter(Mandatory, ParameterSetName = 'FromPath')] [string]$Path, [Parameter(Mandatory, ParameterSetName = 'FromScriptBlock')] [ScriptBlock]$ScriptBlock, [Parameter(Mandatory, ParameterSetName = 'FromCommandName')] [string]$CommandName, [Parameter(Mandatory, Position = 2)] [string]$RuleName ) $ast = switch ($pscmdlet.ParameterSetName) { 'FromString' { [System.Management.Automation.Language.Parser]::ParseInput( $String, [ref]$null, [ref]$null ) } 'FromPath' { $Path = $pscmdlet.GetUnresolvedProviderPathFromPSPath($Path) [System.Management.Automation.Language.Parser]::ParseFile( $Path, [ref]$null, [ref]$null ) } 'FromScriptBlock' { $ScriptBlock.Ast } 'FromCommandName' { try { $command = Get-Command $CommandName -ErrorAction Stop if ($command.CommandType -notin 'ExternalScript', 'Function') { throw [InvalidOperationException]::new('The command "{0}" is not a script or function.' -f $CommandName) } $command.ScriptBlock.Ast } catch { $pscmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( $_.Exception, 'InvalidCommand', 'OperationStopped', $CommandName ) ) } } } # Acquire the type to test try { $astType = (Get-Command $RuleName -ErrorAction Stop).Parameters['ast'].ParameterType } catch { $pscmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( [InvalidOperationException]::new('The name "{0}" is not a valid rule' -f $RuleName, $_.Exception), 'InvalidRuleName', 'OperationStopped', $RuleName ) ) } $predicate = [ScriptBlock]::Create(('param ( $ast ); $ast -is [{0}]' -f $astType.FullName)) foreach ($node in $ast.FindAll($predicate, $true)) { & $RuleName -Ast $node } } #EndRegion '.\public\helper\Invoke-CustomScriptAnalyzerRule.ps1' 93 #Region '.\public\helper\Resolve-ParameterSet.ps1' 0 #using namespace System.Management.Automation function Resolve-ParameterSet { <# .SYNOPSIS Resolve a set of parameter names to a parameter set. .DESCRIPTION Resolve-ParameterSet attempts to discover the parameter set used by a set of named parameters. .EXAMPLE Resolve-ParameterSet -CommandName Invoke-Command -ParameterName ScriptBlock, NoNewScope Find the parameter set name Invoke-Command uses when ScriptBlock and NoNewScope are parameters. .EXAMPLE Resolve-ParameterSet -CommandName Get-Process -ParameterName IncludeUserName Find the parameter set name Get-Process uses when the IncludeUserName parameter is defined. .EXAMPLE Resolve-ParameterSet -CommandName Invoke-Command -ParameterName Session, ArgumentList Writes a non-terminating error noting that no parameter sets matched. #> [CmdletBinding(DefaultParameterSetName = 'FromCommandInfo')] param ( # Attempt to resolve the parameter set for the specified command name. [Parameter(Mandatory, Position = 1, ParameterSetName = 'FromCommandName')] [string]$CommandName, # Attempt to resolve the parameter set for the specified CommandInfo. [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'FromCommandInfo')] [CommandInfo]$CommandInfo, # The parameter names which would be supplied. [AllowEmptyCollection()] [string[]]$ParameterName = @() ) begin { if ($pscmdlet.ParameterSetName -eq 'FromCommandName') { Get-Command $CommandName | Resolve-ParameterSet -ParameterName $ParameterName } } process { if ($pscmdlet.ParameterSetName -eq 'FromCommandInfo') { try { $candidateSets = for ($i = 0; $i -lt $commandInfo.ParameterSets.Count; $i++) { $parameterSet = $commandInfo.ParameterSets[$i] Write-Debug ('Analyzing {0}' -f $parameterSet.Name) $isCandidateSet = $true foreach ($parameter in $parameterSet.Parameters) { if ($parameter.IsMandatory -and -not ($ParameterName -contains $parameter.Name)) { Write-Debug (' Discarded {0}: Missing mandatory parameter {1}' -f $parameterSet.Name, $parameter.Name) $isCandidateSet = $false break } } if ($isCandidateSet) { foreach ($name in $ParameterName) { if ($name -notin $parameterSet.Parameters.Name) { Write-Debug (' Discarded {0}: Parameter {1} is not within set' -f $parameterSet.Name, $parameter.Name) $isCandidateSet = $false break } } } if ($isCandidateSet) { Write-Debug (' Discovered candidate set {0} at index {1}' -f $parameterSet.Name, $i) [PSCustomObject]@{ Name = $parameterSet.Name Index = $i } } } if (@($candidateSets).Count -eq 1) { return $candidateSets.Name } elseif (@($candidateSets).Count -gt 1) { foreach ($parameterSet in $candidateSets) { if ($CommandInfo.ParameterSets[$parameterSet.Index].IsDefault) { return $parameterSet.Name } } $errorRecord = [ErrorRecord]::new( [InvalidOperationException]::new( ('{0}: Ambiguous parameter set: {1}' -f $CommandInfo.Name, ($candidateSets.Name -join ', ') ) ), 'AmbiguousParameterSet', 'InvalidOperation', $ParameterName ) throw $errorRecord } else { $errorRecord = [ErrorRecord]::new( [InvalidOperationException]::new('{0}: Unable to match parameters to a parameter set' -f $CommandInfo.Name), 'CouldNotResolveParameterSet', 'InvalidOperation', $ParameterName ) throw $errorRecord } } catch { Write-Error -ErrorRecord $_ } } } } #EndRegion '.\public\helper\Resolve-ParameterSet.ps1' 121 #Region '.\public\rules\AvoidCreatingObjectsFromAnEmptyString.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language function AvoidCreatingObjectsFromAnEmptyString { <# .SYNOPSIS AvoidCreatingObjectsFromAnEmptyString .DESCRIPTION Objects should not be created by piping an empty string to Select-Object. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [PipelineAst]$ast ) if ($ast.PipelineElements.Count -gt 1) { $isMatchingCase = ( $ast.PipelineElements[0].Expression -is [StringConstantExpressionAst] -and $ast.PipelineElements[0].Expression.SafeGetValue().Trim() -eq '' -and $ast.PipelineElements[1] -is [CommandAst] -and $ast.PipelineElements[1].GetCommandName() -in 'select', 'Select-Object' ) if ($isMatchingCase) { [DiagnosticRecord]@{ Message = 'An empty string is used to create an object with Select-Object in file {0}.' -f $ast.Extent.File Extent = $ast.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' } } } } #EndRegion '.\public\rules\AvoidCreatingObjectsFromAnEmptyString.ps1' 37 #Region '.\public\rules\AvoidDashCharacters.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language function AvoidDashCharacters { <# .SYNOPSIS AvoidDashCharacters .DESCRIPTION Avoid en-dash, em-dash, and horizontal bar outside of strings. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [ScriptBlockAst]$ast ) $ast.FindAll( { param ( $ast ) $shouldCheckAst = ( $ast -is [System.Management.Automation.Language.BinaryExpressionAst] -or $ast -is [System.Management.Automation.Language.CommandParameterAst] -or $ast -is [System.Management.Automation.Language.AssignmentStatementAst] ) if ($shouldCheckAst) { if ($ast.ErrorPosition.Text[0] -in 0x2013, 0x2014, 0x2015) { return $true } } if ($ast -is [System.Management.Automation.Language.CommandAst] -and $ast.GetCommandName() -match '\u2013|\u2014|\u2015') { return $true } }, $false ) | ForEach-Object { [DiagnosticRecord]@{ Message = 'Avoid en-dash, em-dash, and horizontal bar outside of strings.' Extent = $_.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Error' SuggestedCorrections = [CorrectionExtent[]]@( [CorrectionExtent]::new( $_.Extent.StartLineNumber, $_.Extent.EndLineNumber, $_.Extent.StartColumnNumber, $_.Extent.EndColumnNumber, ($_.Extent.Text -replace '\u2013|\u2014|\u2015', '-'), 'Replace dash character' ) ) } } } #EndRegion '.\public\rules\AvoidDashCharacters.ps1' 60 #Region '.\public\rules\AvoidEmptyNamedBlocks.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language function AvoidEmptyNamedBlocks { <# .SYNOPSIS AvoidEmptyNamedBlocks .DESCRIPTION Functions and scripts should not contain empty begin, process, end, or dynamicparam declarations. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [NamedBlockAst]$ast ) process { if ($ast.Statements.Count -eq 0) { [DiagnosticRecord]@{ Message = 'Empty {0} block.' -f $ast.BlockKind Extent = $ast.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' } } } } #EndRegion '.\public\rules\AvoidEmptyNamedBlocks.ps1' 30 #Region '.\public\rules\AvoidFilter.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language function AvoidFilter { <# .SYNOPSIS AvoidFilter .DESCRIPTION Avoid the Filter keyword when creating a function #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [FunctionDefinitionAst]$ast ) if ($ast.IsFilter) { [DiagnosticRecord]@{ Message = 'Avoid the Filter keyword when creating a function' Extent = $ast.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' } } } #EndRegion '.\public\rules\AvoidFilter.ps1' 28 #Region '.\public\rules\AvoidHelpMessage.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language function AvoidHelpMessage { <# .SYNOPSIS AvoidHelpMessage .DESCRIPTION Avoid arguments for boolean values in the parameter attribute. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [AttributeAst]$ast ) if ($ast.TypeName.FullName -eq 'Parameter') { foreach ($namedArgument in $ast.NamedArguments) { if ($namedArgument.ArgumentName -eq 'HelpMessage') { [DiagnosticRecord]@{ Message = 'Avoid using HelpMessage.' Extent = $namedArgument.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' } } } } } #EndRegion '.\public\rules\AvoidHelpMessage.ps1' 32 #Region '.\public\rules\AvoidNestedFunctions.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language function AvoidNestedFunctions { <# .SYNOPSIS AvoidNestedFunctions .DESCRIPTION Functions should not contain nested functions. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [FunctionDefinitionAst]$ast ) $ast.Body.FindAll( { param ( $ast ) $ast -is [FunctionDefinitionAst] }, $true ) | ForEach-Object { [DiagnosticRecord]@{ Message = 'The function {0} in {1} contains the nested function {2}.' -f $ast.Name, $ast.Extent.File, $_.name Extent = $_.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' } } } #EndRegion '.\public\rules\AvoidNestedFunctions.ps1' 37 #Region '.\public\rules\AvoidNewObjectToCreatePSObject.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language function AvoidNewObjectToCreatePSObject { <# .SYNOPSIS AvoidNewObjectToCreatePSObject .DESCRIPTION Functions and scripts should use [PSCustomObject] to create PSObject instances with named properties. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [CommandAst]$ast ) if ($ast.GetCommandName() -eq 'New-Object') { $isPSObject = $ast.CommandElements.Value -contains 'PSObject' if ($isPSObject) { [DiagnosticRecord]@{ Message = 'New-Object is used to create a custom object. Use [PSCustomObject] instead.' Extent = $ast.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' } } } } #EndRegion '.\public\rules\AvoidNewObjectToCreatePSObject.ps1' 32 #Region '.\public\rules\AvoidParameterAttributeDefaultValues.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language function AvoidParameterAttributeDefaultValues { <# .SYNOPSIS AvoidParameterAttributeDefaultValues .DESCRIPTION Avoid including default values in the Parameter attribute. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [AttributeAst]$ast ) if ($ast.TypeName.FullName -eq 'Parameter') { $default = [Parameter]::new() foreach ($namedArgument in $ast.NamedArguments) { if (-not $namedArgument.ExpressionOmitted -and $namedArgument.Argument.SafeGetValue() -eq $default.($namedArgument.ArgumentName)) { [DiagnosticRecord]@{ Message = 'Avoid including default values for {0} in the Parameter attribute.' -f $namedArgument.ArgumentName Extent = $namedArgument.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' } } } } } #EndRegion '.\public\rules\AvoidParameterAttributeDefaultValues.ps1' 34 #Region '.\public\rules\AvoidProcessWithoutPipeline.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language function AvoidProcessWithoutPipeline { <# .SYNOPSIS AvoidProcessWithoutPipeline .DESCRIPTION Functions and scripts should not declare process unless an input pipeline is supported. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [ScriptBlockAst]$ast ) if ($null -ne $ast.ProcessBlock -and $ast.ParamBlock) { $attributeAst = $ast.ParamBlock.Find( { param ( $ast ) $ast -is [AttributeAst] -and $ast.TypeName.Name -eq 'Parameter' -and $ast.NamedArguments.Where{ $_.ArgumentName -in 'ValueFromPipeline', 'ValueFromPipelineByPropertyName' -and $_.Argument.SafeGetValue() -eq $true } }, $false ) if (-not $attributeAst) { [DiagnosticRecord]@{ Message = 'Process declared without an input pipeline' Extent = $ast.ProcessBlock.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' } } } } #EndRegion '.\public\rules\AvoidProcessWithoutPipeline.ps1' 44 #Region '.\public\rules\AvoidRedirectionOperator.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language function AvoidUsingRedirection { <# .SYNOPSIS AvoidUsingRedirection .DESCRIPTION Avoid using redirection to write to files. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [FileRedirectionAst]$ast ) [DiagnosticRecord]@{ Message = 'File redirection is being used to write file content in {0}.' -f $ast.Extent.File Extent = $ast.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' } } #EndRegion '.\public\rules\AvoidRedirectionOperator.ps1' 26 #Region '.\public\rules\AvoidReturnAtEndOfNamedBlock.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language function AvoidReturnAtEndOfNamedBlock { <# .SYNOPSIS AvoidReturnAtEndOfNamedBlock .DESCRIPTION Avoid using return at the end of a named block, when it is the only return statement in a named block. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [NamedBlockAst]$ast ) $returnStatements = $ast.FindAll( { param ( $ast ) $ast -is [ReturnStatementAst] }, $false ) if ($returnStatements.Count -eq 1) { $returnStatement = $returnStatements[0] if ($returnStatement -eq $ast.Statements[-1]) { [DiagnosticRecord]@{ Message = 'Avoid using return when an early end to a named block is not necessary.' Extent = $ast.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' } } } } #EndRegion '.\public\rules\AvoidReturnAtEndOfNamedBlock.ps1' 41 #Region '.\public\rules\AvoidSmartQuotes.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Collections.Generic #using namespace System.Management.Automation.Language function AvoidSmartQuotes { <# .SYNOPSIS AvoidSmartQuotes .DESCRIPTION Avoid smart quotes. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [StringConstantExpressionAst]$ast ) if ($ast.StringConstantType -eq 'BareWord') { return } $normalQuotes = @( "'" '"' ) if ($ast.StringConstantType -in 'DoubleQuotedHereString', 'SingleQuotedHereString') { $startQuote, $endQuote = $ast.Extent.Text[1, -2] } else { $startQuote, $endQuote = $ast.Extent.Text[0, -1] } if ($startQuote -notin $normalQuotes -or $endQuote -notin $normalQuotes) { if ($ast.StringConstantType -in 'SingleQuoted', 'SingleQuotedHereString') { $quoteCharacter = "'" } else { $quoteCharacter = '"' } if ($ast.StringConstantType -like '*HereString') { $startColumnNumber = $ast.Extent.StartColumnNumber + 1 $endColumNumber = $ast.Extent.EndColumnNumber - 2 } else { $startColumnNumber = $ast.Extent.StartColumnNumber $endColumNumber = $ast.Extent.EndColumnNumber - 1 } $corrections = [List[CorrectionExtent]]::new() if ($startQuote -notin $normalQuotes) { $corrections.Add( [CorrectionExtent]::new( $ast.Extent.StartLineNumber, $ast.Extent.StartLineNumber, $startColumnNumber, $startColumnNumber + 1, $quoteCharacter, 'Replace start smart quotes' ) ) } if ($endQuote -notin $normalQuotes) { $corrections.Add( [CorrectionExtent]::new( $ast.Extent.EndLineNumber, $ast.Extent.EndLineNumber, $endColumNumber, $endColumNumber + 1, $quoteCharacter, 'Replace end smart quotes' ) ) } [DiagnosticRecord]@{ Message = 'Avoid smart quotes, always use " or ''.' Extent = $ast.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' SuggestedCorrections = $corrections } } } #EndRegion '.\public\rules\AvoidSmartQuotes.ps1' 85 #Region '.\public\rules\AvoidThrowOutsideOfTry.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language function AvoidThrowOutsideOfTry { <# .SYNOPSIS AvoidThrowOutsideOfTry .DESCRIPTION Advanced functions and scripts should not use throw, except within a try / catch block. Throw is affected by ErrorAction. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [FunctionDefinitionAst]$ast ) $isAdvanced = $null -ne $ast.Body.Find( { param ( $ast ) $ast -is [AttributeAst] -and $ast.TypeName.Name -in 'CmdletBinding', 'Parameter' }, $false ) if (-not $isAdvanced) { return } $namedBlocks = $ast.Body.Find( { param ( $ast ) $ast -is [NamedBlockAst] }, $false ) foreach ($namedBlock in $namedBlocks) { $throwStatements = $namedBlock.FindAll( { param ( $ast ) $ast -is [ThrowStatementAst] }, $false ) if (-not $throwStatements) { return } $tryStatements = $namedBlock.FindAll( { param ( $ast ) $ast -is [TryStatementAst] }, $false ) foreach ($throwStatement in $throwStatements) { if ($tryStatements) { $isWithinExtentOfTry = $false foreach ($tryStatement in $tryStatements) { $isStatementWithinExtentOfTry = ( $throwStatement.Extent.StartOffset -gt $tryStatement.Extent.StartOffset -and $throwStatement.Extent.EndOffset -lt $tryStatement.Extent.EndOffset ) if ($isStatementWithinExtentOfTry) { $isWithinExtentOfTry = $true } } } else { $isWithinExtentOfTry = $false } if (-not $isWithinExtentOfTry) { [DiagnosticRecord]@{ Message = 'throw is used to terminate a function outside of try in the function {0}.' -f $ast.name Extent = $throwStatement.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Error' } } } } } #EndRegion '.\public\rules\AvoidThrowOutsideOfTry.ps1' 94 #Region '.\public\rules\AvoidWriteErrorStop.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language #using namespace System.Management.Automation function AvoidWriteErrorStop { <# .SYNOPSIS AvoidWriteErrorStop .DESCRIPTION Functions and scripts should avoid using Write-Error Stop to terminate a running command or pipeline. The context of the thrown error is Write-Error. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [CommandAst]$ast ) if ($ast.GetCommandName() -eq 'Write-Error') { $parameter = $ast.CommandElements.Where{ $_.ParameterName -like 'ErrorA*' -or $_.ParameterName -eq 'EA' }[0] if ($parameter) { $argumentIndex = $ast.CommandElements.IndexOf($parameter) + 1 $argument = $ast.CommandElements[$argumentIndex].SafeGetValue() if ([Enum]::Parse([ActionPreference], $argument) -eq 'Stop') { [DiagnosticRecord]@{ Message = 'Write-Error is used to create a terminating error. throw or $pscmdlet.ThrowTerminatingError should be used.' Extent = $ast.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' } } } } } #EndRegion '.\public\rules\AvoidWriteErrorStop.ps1' 37 #Region '.\public\rules\AvoidWriteOutput.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language function AvoidWriteOutput { <# .SYNOPSIS AvoidWriteOutput .DESCRIPTION Write-Output does not add significant value to a command. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [CommandAst]$ast ) if ($ast.GetCommandName() -eq 'Write-Output') { [DiagnosticRecord]@{ Message = 'Write-Output is not necessary. Unassigned statements are sent to the output pipeline by default.' Extent = $ast.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' } } } #EndRegion '.\public\rules\AvoidWriteOutput.ps1' 28 #Region '.\public\rules\UseExpressionlessArgumentsInTheParameterAttribute.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language function UseExpressionlessArgumentsInTheParameterAttribute { <# .SYNOPSIS UseExpressionlessArgumentsInTheParameterAttribute .DESCRIPTION Use expressionless arguments for boolean values in the parameter attribute. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [AttributeAst]$ast ) if ($ast.TypeName.FullName -eq 'Parameter') { $parameter = [Parameter]::new() foreach ($namedArgument in $ast.NamedArguments) { if (-not $namedArgument.ExpressionOmitted -and $parameter.($namedArgument.ArgumentName) -is [bool]) { [DiagnosticRecord]@{ Message = 'Use an expressionless named argument for {0}.' -f $namedArgument.ArgumentName Extent = $namedArgument.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' } } } } } #EndRegion '.\public\rules\UseExpressionlessArgumentsInTheParameterAttribute.ps1' 34 #Region '.\public\rules\UseSyntacticallyCorrectExamples.ps1' 0 #using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic #using namespace System.Management.Automation.Language function UseSyntacticallyCorrectExamples { <# .SYNOPSIS UseSyntacticallyCorrectExamples .DESCRIPTION Examples should use parameters described by the function correctly. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'hasTriggered')] [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [FunctionDefinitionAst]$ast ) $definition = [ScriptBlock]::Create($ast.Extent.ToString()) $functionInfo = Get-FunctionInfo -ScriptBlock $definition if ($functionInfo.CmdletBinding) { $helpContent = $ast.GetHelpContent() for ($i = 0; $i -lt $helpContent.Examples.Count; $i++) { $example = $helpContent.Examples[$i] $exampleNumber = $i + 1 $exampleAst = [Parser]::ParseInput( $example, [Ref]$null, [Ref]$null ) $exampleAst.FindAll( { param ( $ast ) $ast -is [CommandAst] }, $false ) | Where-Object { $_.GetCommandName() -eq $ast.Name } | ForEach-Object { $hasTriggered = $false # Non-existant parameters $_.CommandElements | Where-Object { $_ -is [CommandParameterAst] -and $_.ParameterName -notin $functionInfo.Parameters.Keys } | ForEach-Object { $hasTriggered = $true [DiagnosticRecord]@{ Message = 'Example {0} in function {1} uses invalid parameter {2}.' -f @( $exampleNumber $ast.Name $_.ParameterName ) Extent = $ast.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' } } # Only trigger this test if the command includes valid parameters. if (-not $hasTriggered) { # Ambiguous parameter set use try { $parameterName = $_.CommandElements | Where-Object { $_ -is [CommandParameterAst] } | ForEach-Object ParameterName $null = Resolve-ParameterSet -CommandInfo $functionInfo -ParameterName $parameterName -ErrorAction Stop } catch { Write-Debug $_.Exception.Message [DiagnosticRecord]@{ Message = 'Unable to determine parameter set used by example {0} for the function {1}' -f @( $exampleNumber $ast.Name ) Extent = $ast.Extent RuleName = $myinvocation.MyCommand.Name Severity = 'Warning' } } } } } } } #EndRegion '.\public\rules\UseSyntacticallyCorrectExamples.ps1' 90 |