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} contains the nested function {1}.' -f $ast.Name, $_.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
    )

    if ($ast.Parent.Parent.Parent.Parent.IsClass) {
        return
    }

    $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' 45
#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
            try {
                $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'
                    }
                }
            } catch {
                Write-Debug ('Unable to evaluate ErrorAction argument in statement: {0}' -f $ast.Extent.Tostring())
            }
        }
    }
}
#EndRegion '.\public\rules\AvoidWriteErrorStop.ps1' 41
#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
    )

    if ($ast.Parent.Parent.IsClass) {
        return
    }

    $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' 94