PSQualityCheck.psm1

#Region '.\private\ConvertHelp.ps1' -1

function ConvertHelp {
    <#
        .SYNOPSIS
        Convert the help comment into an object
 
        .DESCRIPTION
        Convert the help comment into an object containing all the elements from the help comment
 
        .PARAMETER Help
        A string containing the Help Comment
 
        .EXAMPLE
        $helpObject = ConvertHelp -Help $Help
    #>

    [CmdletBinding()]
    [OutputType([HashTable], [System.Exception])]
    param (
        [parameter(Mandatory = $true)]
        [string]$Help
    )

    # These are the possible Help Comment elements that the script will look for
    # .SYNOPSIS
    # .DESCRIPTION
    # .PARAMETER
    # .EXAMPLE
    # .INPUTS
    # .OUTPUTS
    # .NOTES
    # .LINK
    # .COMPONENT
    # .ROLE
    # .FUNCTIONALITY
    # .FORWARDHELPTARGETNAME
    # .FORWARDHELPCATEGORY
    # .REMOTEHELPRUNSPACE
    # .EXTERNALHELP

    # This function will go through the help and work out which elements are where and what text they contain

    try {

        if (-not(
                $Help.StartsWith("<#") -and
                $Help.EndsWith("#>")
            )) {
            throw "Help does not appear to be a comment block"
        }

        $helpElementsToFind =
        '.SYNOPSIS',
        '.DESCRIPTION',
        '.PARAMETER',
        '.EXAMPLE',
        '.INPUTS',
        '.OUTPUTS',
        '.NOTES',
        '.LINK',
        '.COMPONENT',
        '.ROLE',
        '.FUNCTIONALITY',
        '.FORWARDHELPTARGETNAME',
        '.FORWARDHELPCATEGORY',
        '.REMOTEHELPRUNSPACE',
        '.EXTERNALHELP'

        $commentArray = ($Help -split '\n').Trim()

        $foundElements = @{}
        $numFound = 0
        $lastHelpElement = $null

        for ($line = 0; $line -lt $commentArray.Count; $line++) {

            # get the first 'word' of the help comment. This is required so that we can
            # match '.PARAMETER' since it has a parameter name after it
            $helpElementKey = ($commentArray[$line] -split " ")[0]

            # Get the value of the Help Comment (the parameter name)
            try {
                $helpElementName = ($commentArray[$line] -split " ")[1]
            }
            catch {
                $helpElementName = ""
            }

            if ($helpElementsToFind -contains $helpElementKey) {

                $numFound++

                if ($numFound -ge 2) {

                    # if it's the second element then we must set the help comment text of the
                    # previous element to the found text so far, then reset it

                    $lastElement = @($foundElements[$lastHelpElement])
                    $lastElement[$lastElement.Count - 1].Text = $helpData
                    $foundElements[$lastHelpElement] = $lastElement

                    $helpData = $null
                }

                # this should be an array of HashTables
                # each hash table will contain the properties LineNumber, Name & Text
                $currentElement = @($foundElements[$helpElementKey])

                $newElement = @{}
                $newElement.LineNumber = $line
                $newElement.Name = $helpElementName
                $newElement.Text = ""

                if ($null -eq $currentElement[0]) {
                    $currentElement = $newElement
                }
                else {
                    $currentElement += $newElement
                }

                $foundElements[$helpElementKey] = $currentElement

                $lastHelpElement = $helpElementKey

            }
            else {

                if ($numFound -ge 1 -and $line -lt ($commentArray.Count - 1)) {
                    $helpData += $commentArray[$line]
                }

            }

        }

        if ( -not ([string]::IsNullOrEmpty($lastHelpElement))) {
            $currentElement = @($foundElements[$lastHelpElement])
            $currentElement[$currentElement.Count - 1].Text = $helpData
            $foundElements[$lastHelpElement] = $currentElement
        }

        return $foundElements

    }
    catch {

        throw $_.Exception.Message

    }

}
#EndRegion '.\private\ConvertHelp.ps1' 150
#Region '.\private\ExportFunctionsFromModule.ps1' -1

function ExportFunctionsFromModule {
    <#
        .SYNOPSIS
        Export functions from a PowerShell module (.psm1)
 
        .DESCRIPTION
        Takes a PowerShell module and outputs a single file for each function containing the code for that function
 
        .PARAMETER Path
        A string Path containing the full file name and path to the module
 
        .PARAMETER ExtractPath
        A string Path containing the full path to the extraction folder
 
        .EXAMPLE
        ExportFunctionsFromModule -Path 'c:\path.to\module.psm1' -ExtractPath 'c:\extract'
    #>

    [CmdletBinding()]
    [OutputType([System.Void])]
    param (
        [parameter(Mandatory = $true)]
        [string]$Path,
        [parameter(Mandatory = $true)]
        [string]$ExtractPath
    )

    try {

        $fileProperties = (Get-Item -LiteralPath $Path)

        if ($fileProperties.Extension -ne ".psm1") {
            throw "Passed file does not appear to be a PowerShell module"
        }

        $moduleName = $fileProperties.BaseName

        $ModuleFileContent = Get-Content -Path $Path -ErrorAction Stop

        $ParserErrors = $null
        $ParsedFileFunctions = [System.Management.Automation.PSParser]::Tokenize($ModuleFileContent, [ref]$ParserErrors)

        $ParsedFunctions = ($ParsedFileFunctions | Where-Object { $_.Type -eq "Keyword" -and $_.Content -like 'function' })

        $parsedFunction = 0

        if ($ParsedFunctions.Count -ge 1) {

            $FunctionOutputPath = Join-Path -Path $ExtractPath -ChildPath $moduleName

            if (-not (Test-Path -Path $FunctionOutputPath)) {
                New-Item $FunctionOutputPath -ItemType 'Directory'
            }

            foreach ($Function in $ParsedFunctions) {

                $parsedFunction++

                $FunctionProperties = $ParsedFileFunctions | Where-Object { $_.StartLine -eq $Function.StartLine }
                $FunctionName = ($FunctionProperties | Where-Object { $_.Type -eq "CommandArgument" }).Content

                if ($parsedFunction -eq $ParsedFunctions.Count) {

                    $StartLine = ($Function.StartLine)
                    for ($line = $ModuleFileContent.Count; $line -gt $Function.StartLine; $line--) {
                        if ($ModuleFileContent[$line] -like "}") {
                            $EndLine = $line
                            break
                        }
                    }

                }
                else {

                    $StartLine = ($Function.StartLine)

                    for ($line = $ParsedFunctions[$parsedFunction].StartLine; $line -gt $Function.StartLine; $line--) {
                        if ($ModuleFileContent[$line] -like "}") {
                            $EndLine = $line
                            break
                        }
                    }

                }

                $FunctionOutputFileName = "{0}\{1}{2}" -f $FunctionOutputPath, $FunctionName, ".ps1"

                if (-not (Test-Path -Path $FunctionOutputFileName)) {
                    Out-File -FilePath $FunctionOutputFileName
                }

                for ($line = $StartLine; $line -lt $EndLine; $line++) {
                    Add-Content -Path $FunctionOutputFileName -Value $ModuleFileContent[$line]
                }

            }
        }
        else {
            throw "File contains no functions"
        }
    }
    catch {
        throw
    }
}
#EndRegion '.\private\ExportFunctionsFromModule.ps1' 105
#Region '.\private\GetFileContent.ps1' -1

function GetFileContent {
    <#
        .SYNOPSIS
        Gets the content of a script file
 
        .DESCRIPTION
        Gets the content of the file or the content of the function inside the file
 
        .PARAMETER Path
        A file name to parse
 
        .EXAMPLE
        $fileContent = GetFileContent -Path 'c:\file.txt'
    #>

    [CmdletBinding()]
    [OutputType([System.String[]])]
    param (
        [parameter(Mandatory = $true)]
        [string]$Path
    )

    try {

        $fileContent = Get-Content -Path $Path

        $parserErrors = $null

        if ([string]::IsNullOrEmpty($fileContent)) {
            $parsedFileFunctions = @()
        }
        else {
            ($parsedFileFunctions, $parserErrors) = GetParsedContent -Content $fileContent
        }

        if ($parserErrors -ge 1) {
            throw "Error parsing file"
        }

        $parsedFunctions = @($parsedFileFunctions | Where-Object { $_.Type -eq "Keyword" -and $_.Content -like 'function' })

        if ($parsedFunctions.Count -gt 1) {
            throw "Too many functions in file, file is invalid"
        }

        if ($parsedFunctions.Count -eq 0) {

            for ($line = 0; $line -lt $fileContent.Count; $line++) {

                $parsedFileContent += $fileContent[$line]
                $parsedFileContent += "`r`n"

            }

        }
        else {
            if ($fileContent.Count -gt 1) {

                foreach ($function in $parsedFunctions) {

                    $startLine = ($function.StartLine)

                    for ($line = $fileContent.Count - 1; $line -gt $function.StartLine; $line--) {

                        if ($fileContent[$line] -like "*}*") {

                            $endLine = $line
                            break

                        }

                    }

                    for ($line = $startLine; $line -lt $endLine; $line++) {

                        $parsedFileContent += $fileContent[$line]

                        if ($line -ne ($fileContent.Count - 1)) {
                            $parsedFileContent += "`r`n"
                        }

                    }

                }

            }
            else {

                [int]$startBracket = $fileContent.IndexOf('{')
                [int]$endBracket = $fileContent.LastIndexOf('}')

                $parsedFileContent = $fileContent.substring($startBracket + 1, $endBracket - 1 - $startBracket)

            }
        }

    }
    catch {
        throw
    }

    return $parsedFileContent

}
#EndRegion '.\private\GetFileContent.ps1' 104
#Region '.\private\GetFileList.ps1' -1

function GetFileList {
    <#
        .SYNOPSIS
        Return a list of files
 
        .DESCRIPTION
        Return a list of files from the specified path matching the passed extension
 
        .PARAMETER Path
        A string containing the path
 
        .PARAMETER Extension
        A string containing the extension
 
        .PARAMETER Recurse
        A switch specifying whether or not to recursively search the path specified
 
        .PARAMETER IgnoreFile
        A path to a .psqcignore file (.gitignore file format) for ignoring files
 
        .EXAMPLE
        $files = GetFileList -Path 'c:\folder' -Extension ".ps1"
 
        .EXAMPLE
        $files = GetFileList -Path 'c:\folder' -Extension ".ps1" -Recurse
 
        #.EXAMPLE
        #$files = GetFileList -Path 'c:\folder' -IgnoreFile ".psqcignore"
    #>

    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param (
        [parameter(Mandatory = $true)]
        [string]$Path,
        [parameter(Mandatory = $true)]
        [string]$Extension,
        [parameter(Mandatory = $false)]
        [switch]$Recurse,
        [parameter(Mandatory = $false)]
        [string]$IgnoreFile
    )

    $Extension = $Extension

    if (Test-Path -Path $Path) {

        $FileNameArray = @()

        if ($PSBoundParameters.ContainsKey('IgnoreFile')) {
            $SelectedFilesArray = Get-FilteredChildItem -Path $Path -IgnoreFileName $IgnoreFile
        }
        else {
            $gciSplat = @{
                'Path'    = $Path
                'Exclude' = "*.Tests.*"
            }
            if ($PSBoundParameters.ContainsKey('Recurse')) {
                $gciSplat.Add('Recurse', $true)
            }
            $SelectedFilesArray = Get-ChildItem @gciSplat
        }

        $SelectedFilesArray | Where-Object { $_.Extension -eq $Extension } | Select-Object -Property FullName | ForEach-Object { $FileNameArray += [string]$_.FullName }

    }

    return $FileNameArray

}
#EndRegion '.\private\GetFileList.ps1' 70
#Region '.\private\GetFunctionCount.ps1' -1

function GetFunctionCount {
    <#
        .SYNOPSIS
        Return the count of functions within Module and its Manifest
 
        .DESCRIPTION
        Return the count of functions in the Module and Manifest and whether they appear in their counterpart.
        e.g. Whether the functions in the manifest appear in the module and vice versa
 
        .PARAMETER ModulePath
        A string containing the Module filename
 
        .PARAMETER ManifestPath
        A string containing the Manifest filename
 
        .EXAMPLE
        ($ExportedCommandsCount, $CommandFoundInModuleCount, $CommandInModuleCount, $CommandFoundInManifestCount) = GetFunctionCount -ModulePath $ModulePath -ManifestPath $ManifestPath
 
    #>

    [CmdletBinding()]
    [OutputType([Int[]])]
    param (
        [parameter(Mandatory = $true)]
        [string]$ModulePath,
        [parameter(Mandatory = $true)]
        [string]$ManifestPath
    )

    try {
        if (Test-Path -Path $ManifestPath) {
            $ExportedCommands = (Test-ModuleManifest -Path $ManifestPath -ErrorAction Stop).ExportedCommands
            $ExportedCommandsCount = $ExportedCommands.Count
        }
        else {
            throw "Manifest file doesn't exist"
        }
    }
    catch {
        $ExportedCommands = @()
        $ExportedCommandsCount = 0
    }

    try {
        if (Test-Path -Path $ModulePath) {
            ($ParsedModule, $ParserErrors) = GetParsedFile -Path $ModulePath
        }
        else {
            throw "Module file doesn't exist"
        }
    }
    catch {
        $ParsedModule = @()
        $ParserErrors = 1
    }

    $CommandFoundInModuleCount = 0
    $CommandFoundInManifestCount = 0
    $CommandInModuleCount = 0

    if ( -not ([string]::IsNullOrEmpty($ParsedModule))) {

        foreach ($ExportedCommand in $ExportedCommands.Keys) {

            if ( ($ParsedModule | Where-Object { $_.Type -eq "CommandArgument" -and $_.Content -eq $ExportedCommand })) {

                $CommandFoundInModuleCount++

            }

        }

        $functionNames = @()

        $functionKeywords = ($ParsedModule | Where-Object { $_.Type -eq "Keyword" -and $_.Content -eq "function" })
        $functionKeywords | ForEach-Object {

            $functionLineNo = $_.StartLine
            $functionNames += ($ParsedModule | Where-Object { $_.Type -eq "CommandArgument" -and $_.StartLine -eq $functionLineNo })

        }
    }

    if ($ExportedCommandsCount -ge 1) {

        foreach ($function in $functionNames) {

            $CommandInModuleCount++
            if ($ExportedCommands.ContainsKey($function.Content)) {

                $CommandFoundInManifestCount++

            }

        }

    }

    return ($ExportedCommandsCount, $CommandFoundInModuleCount, $CommandInModuleCount, $CommandFoundInManifestCount)

}
#EndRegion '.\private\GetFunctionCount.ps1' 101
#Region '.\private\GetParsedContent.ps1' -1

function GetParsedContent {
    <#
        .SYNOPSIS
        Get the tokenized content of the passed data
 
        .DESCRIPTION
        Get and return the tokenized content of the passed PowerShell script content
 
        .PARAMETER Content
        A string containing PowerShell script content
 
        .EXAMPLE
        ($ParsedModule, $ParserErrorCount) = GetParsedContent -Content $fileContent
    #>

    [CmdletBinding()]
    [OutputType([System.Object[]], [System.Int32], [System.Void])]
    param (
        [parameter(Mandatory = $true)]
        [System.Object[]]$Content
    )

    if (-not ([string]::IsNullOrEmpty($Content))) {

        $ParserErrors = $null
        $ParsedModule = [System.Management.Automation.PSParser]::Tokenize($Content, [ref]$ParserErrors)

        $ParserErrorCount = $ParserErrors.Count

    }
    else {

        $ParsedModule = $null
        $ParserErrorCount = 1

    }

    return $ParsedModule, $ParserErrorCount

}
#EndRegion '.\private\GetParsedContent.ps1' 40
#Region '.\private\GetParsedFile.ps1' -1

function GetParsedFile {
    <#
        .SYNOPSIS
        Get the tokenized content of the passed file
 
        .DESCRIPTION
        Get and return the tokenized content of the passed PowerShell file
 
        .PARAMETER Path
        A string containing PowerShell filename
 
        .EXAMPLE
        ($ParsedModule, $ParserErrors) = GetParsedFile -Path $ModuleFile
 
    #>

    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param (
        [parameter(Mandatory = $true)]
        [string]$Path
    )

    try {
        if (-not(Test-Path -Path $Path)) {
            throw "$Path doesn't exist"
        }
    }
    catch {
        throw $_
    }

    $fileContent = Get-Content -Path $Path -Raw

    ($ParsedModule, $ParserErrorCount) = GetParsedContent -Content $fileContent

    return $ParsedModule, $ParserErrorCount

}
#EndRegion '.\private\GetParsedFile.ps1' 39
#Region '.\private\GetScriptParameter.ps1' -1

function GetScriptParameter {
    <#
        .SYNOPSIS
        Get a list of the parameters in the param block
 
        .DESCRIPTION
        Create a list of the parameters, and their type (if available) from the param block
 
        .PARAMETER Content
        A string containing the text of the script
 
        .EXAMPLE
        $parameterVariables = GetScriptParameter -Content $Content
    #>

    [CmdletBinding()]
    [OutputType([System.Exception], [HashTable])]
    param (
        [parameter(Mandatory = $true)]
        [String[]]$Content
    )

    try {

        $parsedScript = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$null, [ref]$null)

        if ([string]::IsNullOrEmpty($parsedScript.ParamBlock)) {
            throw "No parameters found"
        }

        [string]$paramBlock = $parsedScript.ParamBlock

        ($ParsedContent, $ParserErrorCount) = GetParsedContent -Content $paramBlock

        $paramBlockArray = ($paramBlock -split '\n').Trim()

        $parametersFound = @{}

        for ($line = 0; $line -le $paramBlockArray.Count; $line++) {

            $paramToken = @($ParsedContent | Where-Object { $_.StartLine -eq $line })

            foreach ($token in $paramToken) {

                if ($token.Type -eq 'Attribute' -and $token.Content -eq "Parameter") {

                    break
                }

                if ($token.Type -eq 'Type') {

                    $foundType = $token.Content

                }

                if ($token.Type -eq 'Variable') {

                    $parametersFound[$token.Content] = $foundType
                    $foundType = $null
                    break

                }

            }

        }

        return $parametersFound

    }
    catch {

        throw $_.Exception.Message

    }

}
#EndRegion '.\private\GetScriptParameter.ps1' 77
#Region '.\private\GetTagList.ps1' -1

function GetTagList {
    <#
        .SYNOPSIS
        Return a list of test tags
 
        .DESCRIPTION
        Return a list of test tags from the module and script checks file
 
        .EXAMPLE
        ($moduleTags, $scriptTags) = GetTagList
 
    #>

    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param (
    )

    $moduleTags = @()
    $scriptTags = @()

    $modulePath = (Get-Module -Name 'PSQualityCheck').ModuleBase

    $checksPath = (Join-Path -Path $modulePath -ChildPath 'data')

    Get-Content -Path (Join-Path -Path $checksPath -ChildPath 'Module.Checks.ps1') -Raw | ForEach-Object {
        $ast = [Management.Automation.Language.Parser]::ParseInput($_, [ref]$null, [ref]$null)
        $ast.FindAll( {
                param($node)
                $node -is [System.Management.Automation.Language.CommandAst] -and
                $node.CommandElements[0].Value -eq 'Describe' -and
                $node.CommandElements[2] -is [System.Management.Automation.Language.CommandParameterAst] -and
                $node.CommandElements[2].ParameterName -eq 'Tag'
            }, $true) | ForEach-Object {
            $moduleTags += $_.CommandElements[3].Value
        }
        $ast.FindAll( {
                param($node)
                $node -is [System.Management.Automation.Language.CommandAst] -and
                $node.CommandElements[0].Value -eq 'It' -and
                $node.CommandElements[2] -is [System.Management.Automation.Language.CommandParameterAst] -and
                $node.CommandElements[2].ParameterName -eq 'Tag'
            }, $true) | ForEach-Object {
            $moduleTags += $_.CommandElements[3].Value
        }
    }

    Get-Content -Path (Join-Path -Path $checksPath -ChildPath 'Script.Checks.ps1') -Raw | ForEach-Object {
        $ast = [Management.Automation.Language.Parser]::ParseInput($_, [ref]$null, [ref]$null)
        $ast.FindAll( {
                param($node)
                $node -is [System.Management.Automation.Language.CommandAst] -and
                $node.CommandElements[0].Value -eq 'Describe' -and
                $node.CommandElements[2] -is [System.Management.Automation.Language.CommandParameterAst] -and
                $node.CommandElements[2].ParameterName -eq 'Tag'
            }, $true) | ForEach-Object {
            $scriptTags += $_.CommandElements[3].Value
        }
        $ast.FindAll( {
                param($node)
                $node -is [System.Management.Automation.Language.CommandAst] -and
                $node.CommandElements[0].Value -eq 'It' -and
                $node.CommandElements[2] -is [System.Management.Automation.Language.CommandParameterAst] -and
                $node.CommandElements[2].ParameterName -eq 'Tag'
            }, $true) | ForEach-Object {
            $scriptTags += $_.CommandElements[3].Value
        }
    }

    return $moduleTags, $scriptTags

}
#EndRegion '.\private\GetTagList.ps1' 72
#Region '.\private\GetToken.ps1' -1

function GetToken {
    <#
        .SYNOPSIS
        Get token(s) from the tokenized output
 
        .DESCRIPTION
        Get token(s) from the tokenized output matching the passed Type and Content
 
        .PARAMETER ParsedContent
        A string array containing the Tokenized data
 
        .PARAMETER Type
        The token type to be found
 
        .PARAMETER Content
        The token content (or value) to be found
 
        .EXAMPLE
        $outputTypeToken = (GetToken -ParsedContent $ParsedFile -Type "Attribute" -Content "OutputType")
    #>

    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param (
        [parameter(Mandatory = $true)]
        [System.Object[]]$ParsedContent,
        [parameter(Mandatory = $true)]
        [string]$Type,
        [parameter(Mandatory = $true)]
        [string]$Content
    )

    $token = GetTokenMarker -ParsedContent $ParsedContent -Type $Type -Content $Content

    $tokens = GetTokenComponent -ParsedContent $ParsedContent -StartLine $token.StartLine

    return $tokens

}
#EndRegion '.\private\GetToken.ps1' 39
#Region '.\private\GetTokenComponent.ps1' -1

function GetTokenComponent {
    <#
        .SYNOPSIS
        Get all the tokens components from a single line
 
        .DESCRIPTION
        Get all the tokens components from a single line in the tokenized content
 
        .PARAMETER ParsedContent
        A string array containing the tokenized content
 
        .PARAMETER StartLine
        A integer of the starting line to parse
 
        .EXAMPLE
        $tokens = GetTokenComponent -ParsedContent $ParsedContent -StartLine 10
    #>

    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param (
        [parameter(Mandatory = $true)]
        [System.Object[]]$ParsedContent,
        [parameter(Mandatory = $true)]
        [int]$StartLine
    )

    #* This is just to satisfy the PSScriptAnalyzer
    #* which can't find the variables in the 'Where-Object' clause (even though it's valid)
    $StartLine = $StartLine

    $tokenComponents = @($ParsedContent | Where-Object { $_.StartLine -eq $StartLine })

    return $tokenComponents

}
#EndRegion '.\private\GetTokenComponent.ps1' 36
#Region '.\private\GetTokenMarker.ps1' -1

function GetTokenMarker {
    <#
        .SYNOPSIS
        Gets token from the tokenized output
 
        .DESCRIPTION
        Gets single token from the tokenized output matching the passed Type and Content
 
        .PARAMETER ParsedContent
        A string array containing the Tokenized data
 
        .PARAMETER Type
        The token type to be found
 
        .PARAMETER Content
        The token content (or value) to be found
 
        .EXAMPLE
        $token = GetTokenMarker -ParsedContent $ParsedContent -Type $Type -Content $Content
    #>

    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param (
        [parameter(Mandatory = $true)]
        [System.Object[]]$ParsedContent,
        [parameter(Mandatory = $true)]
        [string]$Type,
        [parameter(Mandatory = $true)]
        [string]$Content
    )

    #* This is just to satisfy the PSScriptAnalyzer
    #* which can't find the variables in the 'Where-Object' clause (even though it's valid)
    $Type = $Type
    $Content = $Content

    $token = @($ParsedContent | Where-Object { $_.Type -eq $Type -and $_.Content -eq $Content })

    return $token

}
#EndRegion '.\private\GetTokenMarker.ps1' 42
#Region '.\private\TestHelpTokensCountisValid.ps1' -1

function TestHelpTokensCountIsValid {
    <#
        .SYNOPSIS
        Check that help tokens count is valid
 
        .DESCRIPTION
        Check that the help tokens count is valid by making sure that they appear between Min and Max times
 
        .PARAMETER HelpTokens
        A array of tokens containing the tokens of the Help Comment
 
        .PARAMETER HelpRulesPath
        Path to the HelpRules file
 
        .EXAMPLE
        TestHelpTokensCountIsValid -HelpTokens $HelpTokens -HelpRulesPath "C:\HelpRules"
 
        .NOTES
        This function will only check the Min/Max counts of required help tokens
    #>

    [CmdletBinding()]
    [OutputType([System.Exception], [System.Void])]
    param (
        [parameter(Mandatory = $true)]
        [HashTable]$HelpTokens,

        [parameter(Mandatory = $true)]
        [string]$HelpRulesPath
    )

    try {

        $helpRules = Import-PowerShellDataFile -Path $HelpRulesPath

        $tokenFound = @{}
        for ($order = 1; $order -le $HelpRules.Count; $order++) {
            $helpRuleIndex = [string]$order
            $token = $HelpRules.$helpRuleIndex.Key
            $tokenFound[$token] = $false
        }

        $tokenErrors = @()

        foreach ($key in $HelpTokens.Keys) {

            for ($order = 1; $order -le $HelpRules.Count; $order++) {

                $helpRuleIndex = [string]$order
                $token = $HelpRules.$helpRuleIndex

                if ( $token.Key -eq $key ) {

                    $tokenFound[$key] = $true

                    if ($HelpTokens.$key.Count -lt $token.MinOccurrences -or
                        $HelpTokens.$key.Count -gt $token.MaxOccurrences -and
                        $token.Required -eq $true) {

                        $tokenErrors += "Found $(($HelpTokens.$key).Count) occurrences of '$key' which is not between $($token.MinOccurrences) and $($token.MaxOccurrences). "

                    }

                }

            }

        }

        if ($tokenErrors.Count -ge 1) {

            throw $tokenErrors

        }

    }
    catch {

        throw $_.Exception.Message

    }

}
#EndRegion '.\private\TestHelpTokensCountisValid.ps1' 83
#Region '.\private\TestHelpTokensParamsMatch.ps1' -1

function TestHelpTokensParamsMatch {
    <#
        .SYNOPSIS
        Checks to see whether the parameters and help PARAMETER statements match
 
        .DESCRIPTION
        Checks to see whether the parameters in the param block and in the help PARAMETER statements exist in both locations
 
        .PARAMETER HelpTokens
        A array of tokens containing the tokens of the Help Comment
 
        .PARAMETER ParameterVariables
        A object containing the parameters from the param block
 
        .EXAMPLE
        TestHelpTokensParamsMatch -HelpTokens $HelpTokens -ParameterVariables $ParameterVariables
    #>

    [CmdletBinding()]
    [OutputType([System.Exception], [System.String[]])]
    param (
        [parameter(Mandatory = $true)]
        [HashTable]$HelpTokens,
        [parameter(Mandatory = $true)]
        [PSCustomObject]$ParameterVariables
    )

    try {

        $foundInHelpErrors = @()
        $foundInParamErrors = @()

        foreach ($key in $ParameterVariables.Keys) {

            $foundInHelp = $false

            foreach ($token in $HelpTokens.".PARAMETER") {

                if ($key -eq $token.Name) {

                    $foundInHelp = $true
                    break

                }

            }

            if ($foundInHelp -eq $false) {

                $foundInHelpErrors += "Parameter block variable '$key' was not found in help. "

            }

        }

        foreach ($token in $HelpTokens.".PARAMETER") {

            $foundInParams = $false

            foreach ($key in $ParameterVariables.Keys) {

                if ($key -eq $token.Name) {

                    $foundInParams = $true
                    break

                }

            }

            if ($foundInParams -eq $false) {

                $foundInParamErrors += "Help defined variable '$($token.Name)' was not found in parameter block definition. "

            }

        }

        if ($foundInHelpErrors.Count -ge 1 -or $foundInParamErrors.Count -ge 1) {

            $allErrors = $foundInHelpErrors + $foundInParamErrors
            throw $allErrors

        }

    }
    catch {

        throw $_.Exception.Message

    }

}
#EndRegion '.\private\TestHelpTokensParamsMatch.ps1' 93
#Region '.\private\TestHelpTokensTextIsValid.ps1' -1

function TestHelpTokensTextIsValid {
    <#
        .SYNOPSIS
        Check that Help Tokens text is valid
 
        .DESCRIPTION
        Check that the Help Tokens text is valid by making sure that they its not empty
 
        .PARAMETER HelpTokens
        A array of tokens containing the tokens of the Help Comment
 
        .EXAMPLE
        TestHelpTokensTextIsValid -HelpTokens $HelpTokens
    #>

    [CmdletBinding()]
    [OutputType([System.Exception], [System.Void])]
    param (
        [parameter(Mandatory = $true)]
        [HashTable]$HelpTokens
    )

    try {

        $tokenErrors = @()

        foreach ($key in $HelpTokens.Keys) {

            $tokenCount = @($HelpTokens.$key)

            for ($loop = 0; $loop -lt $tokenCount.Count; $loop++) {

                $token = $HelpTokens.$key[$loop]

                if ([string]::IsNullOrWhitespace($token.Text)) {

                    $tokenErrors += "Found '$key' does not have any text. "

                }

            }

        }

        if ($tokenErrors.Count -ge 1) {

            throw $tokenErrors

        }

    }
    catch {

        throw $_.Exception.Message

    }

}
#EndRegion '.\private\TestHelpTokensTextIsValid.ps1' 58
#Region '.\private\TestImportModuleIsValid.ps1' -1

function TestImportModuleIsValid {
    <#
        .SYNOPSIS
        Test that the Import-Module commands are valid
 
        .DESCRIPTION
        Test that the Import-Module commands contain a -Name parameter, and one of RequiredVersion, MinimumVersion or MaximumVersion
 
        .PARAMETER ParsedContent
        An object containing the source file parsed into its Tokenizer components
 
        .PARAMETER ImportModuleTokens
        An object containing the Import-Module tokens found
 
        .EXAMPLE
        TestImportModuleIsValid -ParsedContent $ParsedContent -ImportModuleTokens $importModuleTokens
    #>

    [CmdletBinding()]
    [OutputType([System.Exception], [System.Void])]
    param (
        [parameter(Mandatory = $true)]
        [System.Object[]]$ParsedContent,
        [parameter(Mandatory = $true)]
        [System.Object[]]$ImportModuleTokens
    )

    try {

        $errString = ""

        foreach ($token in $importModuleTokens) {

            $importModuleStatement = GetTokenComponent -ParsedContent $ParsedContent -StartLine $token.StartLine

            try {
                $name = ($importModuleStatement | Where-Object { $_.Type -eq "CommandArgument" } | Select-Object -First 1).Content
            }
            catch {
                $name = $null
            }
            if ($null -eq $name) {

                $name = ($importModuleStatement | Where-Object { $_.Type -eq "String" } | Select-Object -First 1).Content

            }

            if (-not($importModuleStatement | Where-Object { $_.Type -eq "CommandParameter" -and $_.Content -eq "-Name" })) {

                $errString += "Import-Module for '$name' : Missing -Name parameter keyword. "

            }

            if (-not($importModuleStatement | Where-Object { $_.Type -eq "CommandParameter" -and
                        ( $_.Content -eq "-RequiredVersion" -or $_.Content -eq "-MinimumVersion" -or $_.Content -eq "-MaximumVersion" )
                    })) {

                $errString += "Import-Module for '$name' : Missing -RequiredVersion, -MinimumVersion or -MaximumVersion parameter keyword. "

            }

        }

        if (-not ([string]::IsNullOrEmpty($errString))) {

            throw $errString

        }

    }
    catch {

        throw $_.Exception.Message

    }

}
#EndRegion '.\private\TestImportModuleIsValid.ps1' 77
#Region '.\private\TestParameterVariablesHaveType.ps1' -1

function TestParameterVariablesHaveType {
    <#
        .SYNOPSIS
        Check that all the passed parameters have a type variable set.
 
        .DESCRIPTION
        Check that all the passed parameters have a type variable set.
 
        .PARAMETER ParameterVariables
        A HashTable containing the parameters from the param block
 
        .EXAMPLE
        TestParameterVariablesHaveType -ParameterVariables $ParameterVariables
    #>

    [CmdletBinding()]
    [OutputType([System.Exception], [System.Void])]
    param (
        [parameter(Mandatory = $true)]
        [HashTable]$ParameterVariables
    )

    $variableErrors = @()

    try {

        foreach ($key in $ParameterVariables.Keys) {

            if ([string]::IsNullOrEmpty($ParameterVariables.$key)) {

                $variableErrors += "Parameter '$key' does not have a type defined."

            }

        }

        if ($variableErrors.Count -ge 1) {

            throw $variableErrors
        }

    }
    catch {

        throw $_.Exception.Message

    }

}
#EndRegion '.\private\TestParameterVariablesHaveType.ps1' 49
#Region '.\private\TestRequiredToken.ps1' -1

function TestRequiredToken {
    <#
        .SYNOPSIS
        Check that help tokens contain required tokens
 
        .DESCRIPTION
        Check that the help comments contain tokens that are specified in the external verification data file
 
        .PARAMETER HelpTokens
        A array of tokens containing the tokens of the Help Comment
 
        .PARAMETER HelpRulesPath
        Path to the HelpRules file
 
        .EXAMPLE
        TestRequiredToken -HelpTokens $HelpTokens -HelpRulesPath "C:\HelpRules"
    #>

    [CmdletBinding()]
    [OutputType([System.Exception], [System.Void])]
    param (
        [parameter(Mandatory = $true)]
        [HashTable]$HelpTokens,

        [parameter(Mandatory = $true)]
        [string]$HelpRulesPath
    )

    try {

        $helpRules = Import-PowerShellDataFile -Path $HelpRulesPath

        $tokenErrors = @()

        for ($order = 1; $order -le $HelpRules.Count; $order++) {

            $helpRuleIndex = [string]$order
            $token = $HelpRules.$helpRuleIndex

            if ($token.Key -notin $HelpTokens.Keys ) {

                if ($token.Required -eq $true) {

                    $tokenErrors += $token.Key

                }

            }

        }

        if ($tokenErrors.Count -ge 1) {
            throw "Missing required token(s): $tokenErrors"
        }

    }
    catch {

        throw $_.Exception.Message

    }

}
#EndRegion '.\private\TestRequiredToken.ps1' 63
#Region '.\private\TestUnspecifiedToken.ps1' -1

function TestUnspecifiedToken {
    <#
        .SYNOPSIS
        Check that help tokens do not contain unspecified tokens
 
        .DESCRIPTION
        Check that the help comments do not contain tokens that are not specified in the external verification data file
 
        .PARAMETER HelpTokens
        A array of tokens containing the tokens of the Help Comment
 
        .PARAMETER HelpRulesPath
        Path to the HelpRules file
 
        .EXAMPLE
        TestUnspecifiedToken -HelpTokens $HelpTokens -HelpRulesPath "C:\HelpRules"
    #>

    [CmdletBinding()]
    [OutputType([System.Exception], [System.Void])]
    param (
        [parameter(Mandatory = $true)]
        [HashTable]$HelpTokens,

        [parameter(Mandatory = $true)]
        [string]$HelpRulesPath
    )

    try {

        $helpRules = Import-PowerShellDataFile -Path $HelpRulesPath

        $tokenErrors = @()
        $helpTokensKeys = @()

        for ($order = 1; $order -le $helpRules.Count; $order++) {

            $helpRuleIndex = [string]$order
            $token = $helpRules.$helpRuleIndex

            $helpTokensKeys += $token.key

        }

        foreach ($key in $helpTokens.Keys) {

            if ( $key -notin $helpTokensKeys ) {

                $tokenErrors += $key

            }

        }

        if ($tokenErrors.Count -ge 1) {
            throw "Found extra, non-specified, token(s): $tokenErrors"
        }

    }
    catch {

        throw $_.Exception.Message

    }

}
#EndRegion '.\private\TestUnspecifiedToken.ps1' 66
#Region '.\Public\Invoke-PSQualityCheck.ps1' -1

function Invoke-PSQualityCheck {
    <#
        .SYNOPSIS
        Invoke the PSQualityCheck tests
 
        .DESCRIPTION
        Invoke a series of Pester-based quality tests on the passed files
 
        .PARAMETER Path
        A string array containing paths to check for testable files
 
        .PARAMETER File
        A string array containing testable files
 
        .PARAMETER Recurse
        A switch specifying whether or not to recursively search the path specified
 
        .PARAMETER ScriptAnalyzerRulesPath
        A path the the external PSScriptAnalyzer rules
 
        .PARAMETER ShowCheckResults
        Show a summary of the Check results at the end of processing
        Note: this cannot be used with -Passthru
 
        .PARAMETER ExportCheckResults
        Exports the Check results at the end of processing to file
 
        .PARAMETER Passthru
        Returns the Check results objects back to the caller
        Note: this cannot be used with -ShowCheckResults
 
        .PARAMETER PesterConfiguration
        A Pester configuration object to allow configuration of Pester
 
        .PARAMETER Include
        An array of test tags to run
 
        .PARAMETER Exclude
        An array of test tags to not run
 
        .PARAMETER ProjectPath
        A path to the root of a Project
 
        .PARAMETER HelpRulesPath
        A path to the HelpRules parameter file
 
        .PARAMETER IgnoreFile
        A path to the .psqcignore file which excludes files/path from the tests. This is in the .gitignore format
 
        .EXAMPLE
        Invoke-PSQualityCheck -Path 'C:\Scripts'
 
        This will call the quality checks on single path
 
        .EXAMPLE
        Invoke-PSQualityCheck -Path 'C:\Scripts' -Recurse
 
        This will call the quality checks on single path and sub folders
 
        .EXAMPLE
        Invoke-PSQualityCheck -Path @('C:\Scripts', 'C:\MoreScripts')
 
        This will call the quality checks with multiple paths
 
        .EXAMPLE
        Invoke-PSQualityCheck -ProjectPath 'C:\Project' -IgnoreFile ".psqcignore"
 
        This will call the project quality checks on the C:\Project folder with the .psqcignore file
 
        .EXAMPLE
        Invoke-PSQualityCheck -File 'C:\Scripts\Script.ps1'
 
        This will call the quality checks with single script file
 
        .EXAMPLE
        Invoke-PSQualityCheck -File 'C:\Scripts\Script.psm1'
 
        This will call the quality checks with single module file
 
        .EXAMPLE
        Invoke-PSQualityCheck -File 'C:\Scripts\Script.psd1'
 
        This will call the quality checks with single datafile file
        Note: The datafile test will fail as it is not a file that is accepted for testing
 
        .EXAMPLE
        Invoke-PSQualityCheck -File @('C:\Scripts\Script.ps1','C:\Scripts\Script2.ps1')
 
        This will call the quality checks with multiple files. Files can be either scripts or modules
 
        .EXAMPLE
        Invoke-PSQualityCheck -File 'C:\Scripts\Script.ps1' -ScriptAnalyzerRulesPath 'C:\ScriptAnalyzerRulesPath'
 
        This will call the quality checks with single file and the extra PSScriptAnalyzer rules
 
        .EXAMPLE
        Invoke-PSQualityCheck -Path 'C:\Scripts' -ShowCheckResults
 
        This will display a summary of the checks performed (example below uses sample data):
 
            Name Files Tested Total Passed Failed Skipped
            ---- ------------ ----- ------ ------ -------
            Module Tests 2 14 14 0 0
            Extracting functions 2 2 2 0 0
            Extracted function script tests 22 330 309 0 21
            Total 24 346 325 0 21
 
        For those who have spotted that the Total files tested isn't a total of the rows above, this is because the Module Tests and Extracting function Tests operate on the same file and are not counted twice
 
        .LINK
        Website: https://github.com/andrewrdavidson/PSQualityCheck
        SonarQube rules are available here: https://github.com/indented-automation/ScriptAnalyzerRules
 
    #>

    [CmdletBinding()]
    [OutputType([System.Void], [HashTable], [System.Object[]])]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Path')]
        [String[]]$Path,
        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [String[]]$File,
        [Parameter(Mandatory = $true, ParameterSetName = 'ProjectPath')]
        [String]$ProjectPath,

        [Parameter(Mandatory = $false, ParameterSetName = 'Path')]
        [switch]$Recurse,

        [Parameter(Mandatory = $false)]
        [String[]]$ScriptAnalyzerRulesPath,

        [switch]$ShowCheckResults,

        [switch]$ExportCheckResults,

        [switch]$Passthru,

        [Parameter(Mandatory = $false)]
        [System.Object]$PesterConfiguration,

        [Parameter(Mandatory = $false)]
        [String[]]$Include,

        [Parameter(Mandatory = $false)]
        [String[]]$Exclude,

        [Parameter(Mandatory = $false)]
        [String]$HelpRulesPath,

        [Parameter(Mandatory = $false)]
        [String]$IgnoreFile

    )

    Set-StrictMode -Version Latest

    # External Modules
    Import-Module -Name 'Pester' -MinimumVersion '5.1.0' -Force
    Import-Module -Name 'PSScriptAnalyzer' -MinimumVersion '1.19.1' -Force

    $modulePath = (Get-Module -Name 'PSQualityCheck').ModuleBase

    # Validate any incoming parameters for clashes
    if ($PSBoundParameters.ContainsKey('ShowCheckResults') -and $PSBoundParameters.ContainsKey('Passthru')) {

        Write-Error '-ShowCheckResults and -Passthru cannot be used at the same time'
        break

    }

    $scriptsToTest = @()
    $modulesToTest = @()

    $projectResults = $null
    $moduleResults = $null
    $extractionResults = $null
    $extractedScriptResults = $null
    $scriptResults = $null

    if ($PSBoundParameters.ContainsKey('HelpRulesPath')) {

        if ( -not (Test-Path -Path $HelpRulesPath)) {

            Write-Error '-HelpRulesPath does not exist'
            break

        }

    } else {

        $helpRulesPath = (Join-Path -Path $modulePath -ChildPath 'Data\HelpRules.psd1')

    }

    if ($PSBoundParameters.ContainsKey('PesterConfiguration') -and $PesterConfiguration -is [PesterConfiguration]) {

        # left here so that we can over-ride passed in object with values we require

    } else {
        # Default Pester Parameters
        $PesterConfiguration = [PesterConfiguration]::Default
        $PesterConfiguration.Run.Exit = $false
        $PesterConfiguration.CodeCoverage.Enabled = $false
        $PesterConfiguration.Output.Verbosity = 'Detailed'
        $PesterConfiguration.Run.PassThru = $true
        $PesterConfiguration.Should.ErrorAction = 'Stop'
    }

    # Analyse the incoming Path and File parameters and produce a list of Modules and Scripts
    if ($PSBoundParameters.ContainsKey('Path') -or $PSBoundParameters.ContainsKey('ProjectPath')) {

        if ($PSBoundParameters.ContainsKey('ProjectPath')) {

            if (Test-Path -Path $ProjectPath) {

                $container1 = New-PesterContainer -Path (Join-Path -Path $modulePath -ChildPath 'Data\Project.Checks.ps1') -Data @{ Path = $ProjectPath }
                $PesterConfiguration.Run.Container = $container1
                $projectResults = Invoke-Pester -Configuration $PesterConfiguration

                # setup the rest of the Path based tests
                # this needs to include the module/public module/private and scripts folder only, not the data or any other folders
                $ProjectPath = (Resolve-Path -Path $ProjectPath)
                $sourceRootPath = (Join-Path -Path $ProjectPath -ChildPath 'Source')
                $scriptPath = (Join-Path -Path $ProjectPath -ChildPath 'Scripts')
                $Path = @($scriptPath)

                # Get the list of Modules
                $modules = Get-ChildItem -Path $sourceRootPath -Directory

                foreach ($module in $modules) {

                    $Path += (Join-Path -Path $module -ChildPath 'Public')
                    $Path += (Join-Path -Path $module -ChildPath 'Private')

                }

            } else {
                Write-Error "Project Path $ProjectPath does not exist"
                $projectResults =
                @{
                    'Count'        = 0
                    'TotalCount'   = 0
                    'NotRunCount'  = 0
                    'PassedCount'  = 0
                    'FailedCount'  = 0
                    'SkippedCount' = 0
                }
            }

        }

        if ($Path -isnot [string[]]) {
            $Path = @($Path)
        }

        foreach ($item in $Path) {

            # Test whether the item is a directory (also tells us if it exists)
            if (Test-Path -Path $item -PathType Container) {

                $getFileListSplat = @{
                    'Path' = $item
                }

                if ($PSBoundParameters.ContainsKey('IgnoreFile')) {
                    $getFileListSplat.Add('IgnoreFile', (Resolve-Path -Path $IgnoreFile))
                } else {
                    if ($PSBoundParameters.ContainsKey('Recurse') -or
                        $PSBoundParameters.ContainsKey('ProjectPath')) {
                        $getFileListSplat.Add('Recurse', $true)
                    }
                }

                $scriptsToTest += GetFileList @getFileListSplat -Extension '.ps1'
                $modulesToTest += GetFileList @getFileListSplat -Extension '.psm1'

            } else {

                Write-Warning -Message "$item is not a directory, skipping"

            }

        }

    }

    if ($PSBoundParameters.ContainsKey('File')) {

        if ($File -isnot [string[]]) {
            $File = @($File)
        }

        foreach ($item in $File) {

            # Test whether the item is a file (also tells us if it exists)
            if (Test-Path -Path $item -PathType Leaf) {

                $getFilteredItemSplat = @{
                    'Path' = $item
                }

                if ($PSBoundParameters.ContainsKey('IgnoreFile')) {
                    $getFilteredItemSplat.Add('IgnoreFile', (Resolve-Path -Path $IgnoreFile))
                }

                $itemProperties = Get-FilteredChildItem @getFilteredItemSplat

                switch ($itemProperties.Extension) {

                    '.psm1' {
                        $modulesToTest += $itemProperties
                    }

                    '.ps1' {
                        $scriptsToTest += $itemProperties
                    }

                }

            } else {

                Write-Warning -Message "$item is not a file, skipping"

            }

        }

    }

    # Get the list of test tags from the Data files
    if ($PSBoundParameters.ContainsKey('Include') -or
        $PSBoundParameters.ContainsKey('Exclude')) {


        ($moduleTags, $scriptTags) = GetTagList
        $moduleTagsToInclude = @()
        $moduleTagsToExclude = @()
        $scriptTagsToInclude = @()
        $scriptTagsToExclude = @()
        $runModuleCheck = $false
        $runScriptCheck = $false

    } else {
        $runModuleCheck = $true
        $runScriptCheck = $true
    }

    if ($PSBoundParameters.ContainsKey('Include')) {

        if ($Include -eq 'All') {
            $moduleTagsToInclude = $moduleTags
            $scriptTagsToInclude = $scriptTags
            $runModuleCheck = $true
            $runScriptCheck = $true
        } else {
            # Validate tests to include from $Include
            $Include | ForEach-Object {
                if ($_ -in $moduleTags) {
                    $moduleTagsToInclude += $_
                    $runModuleCheck = $true
                    #* To satisfy PSScriptAnalyzer
                    $runModuleCheck = $runModuleCheck
                    $runScriptCheck = $runScriptCheck
                }
            }
            $Include | ForEach-Object {
                if ($_ -in $scriptTags) {
                    $scriptTagsToInclude += $_
                    $runScriptCheck = $true
                    #* To satisfy PSScriptAnalyzer
                    $runModuleCheck = $runModuleCheck
                    $runScriptCheck = $runScriptCheck
                }
            }
        }
        $PesterConfiguration.Filter.Tag = $moduleTagsToInclude + $scriptTagsToInclude

    }

    if ($PSBoundParameters.ContainsKey('Exclude')) {

        # Validate tests to exclude from $Exclude
        $Exclude | ForEach-Object {
            if ($_ -in $moduleTags) {
                $moduleTagsToExclude += $_
                $runModuleCheck = $true
                #* To satisfy PSScriptAnalyzer
                $runModuleCheck = $runModuleCheck
                $runScriptCheck = $runScriptCheck
            }
        }
        $Exclude | ForEach-Object {
            if ($_ -in $scriptTags) {
                $scriptTagsToExclude += $_
                $runScriptCheck = $true
                #* To satisfy PSScriptAnalyzer
                $runModuleCheck = $runModuleCheck
                $runScriptCheck = $runScriptCheck
            }
        }
        $PesterConfiguration.Filter.ExcludeTag = $moduleTagsToExclude + $scriptTagsToExclude

    }

    if ($modulesToTest.Count -ge 1) {

        # Location of files extracted from any passed modules
        $extractPath = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath (New-Guid).Guid

        if ($runModuleCheck -eq $true) {

            # Run the Module tests on all the valid module files found
            $container1 = New-PesterContainer -Path (Join-Path -Path $modulePath -ChildPath 'Data\Module.Checks.ps1') -Data @{ Source = $modulesToTest }
            $PesterConfiguration.Run.Container = $container1
            $moduleResults = Invoke-Pester -Configuration $PesterConfiguration

            # Extract all the functions from the modules into individual .ps1 files ready for testing
            $container2 = New-PesterContainer -Path (Join-Path -Path $modulePath -ChildPath 'Data\Extraction.ps1') -Data @{ Source = $modulesToTest; ExtractPath = $extractPath }
            $PesterConfiguration.Run.Container = $container2
            $extractionResults = Invoke-Pester -Configuration $PesterConfiguration

        } else {
            $moduleResults =
            @{
                'Count'        = 0
                'TotalCount'   = 0
                'NotRunCount'  = 0
                'PassedCount'  = 0
                'FailedCount'  = 0
                'SkippedCount' = 0
            }
            $extractionResults =
            @{
                'Count'        = 0
                'TotalCount'   = 0
                'NotRunCount'  = 0
                'PassedCount'  = 0
                'FailedCount'  = 0
                'SkippedCount' = 0
            }
        }

        if ($runScriptCheck -eq $true -and (Test-Path -Path $extractPath -ErrorAction SilentlyContinue)) {

            # Get a list of the 'extracted' function scripts .ps1 files
            $extractedScriptsToTest = Get-ChildItem -Path $extractPath -Include '*.ps1' -Recurse

            # Run the Script tests against all the extracted functions .ps1 files
            $container3 = New-PesterContainer -Path (Join-Path -Path $modulePath -ChildPath 'Data\Script.Checks.ps1') -Data @{ Source = $extractedScriptsToTest; ScriptAnalyzerRulesPath = $ScriptAnalyzerRulesPath; HelpRulesPath = $HelpRulesPath }
            $PesterConfiguration.Run.Container = $container3
            $extractedScriptResults = Invoke-Pester -Configuration $PesterConfiguration

        } else {
            $extractedScriptResults =
            @{
                'Count'        = 0
                'TotalCount'   = 0
                'NotRunCount'  = 0
                'PassedCount'  = 0
                'FailedCount'  = 0
                'SkippedCount' = 0
            }
        }

        # Tidy up and temporary paths that have been used

        if ( Test-Path -Path $ExtractPath -ErrorAction SilentlyContinue) {
            Get-ChildItem -Path $ExtractPath -Recurse -ErrorAction SilentlyContinue | Remove-Item -Force -Recurse
            Remove-Item $ExtractPath -Force -ErrorAction SilentlyContinue
        }

    }

    if ($scriptsToTest.Count -ge 1 -and $runScriptCheck -eq $true) {

        # Run the Script tests against all the valid script files found
        $container3 = New-PesterContainer -Path (Join-Path -Path $modulePath -ChildPath 'Data\Script.Checks.ps1') -Data @{ Source = $scriptsToTest; ScriptAnalyzerRulesPath = $ScriptAnalyzerRulesPath; HelpRulesPath = $HelpRulesPath }
        $PesterConfiguration.Run.Container = $container3
        $scriptResults = Invoke-Pester -Configuration $PesterConfiguration

    } else {
        $scriptResults =
        @{
            'Count'        = 0
            'TotalCount'   = 0
            'NotRunCount'  = 0
            'PassedCount'  = 0
            'FailedCount'  = 0
            'SkippedCount' = 0
        }
    }

    # Show/Export results in the various formats

    if ($PSBoundParameters.ContainsKey('ShowCheckResults')) {

        $qualityCheckResults = @()
        $filesTested = $total = $passed = $failed = $skipped = 0

        if ($null -ne $projectResults) {
            $qualityCheckResults +=
            @{
                'Test'         = 'Project Tests'
                'Files Tested' = 0
                'Total'        = ($projectResults.TotalCount - $projectResults.NotRunCount)
                'Passed'       = $projectResults.PassedCount
                'Failed'       = $projectResults.FailedCount
                'Skipped'      = $projectResults.SkippedCount
            }
            $filesTested += 0
            $total += ($projectResults.TotalCount - $projectResults.NotRunCount)
            $passed += $projectResults.PassedCount
            $failed += $projectResults.FailedCount
            $skipped += $projectResults.SkippedCount
        }

        if ($null -ne $moduleResults) {
            $qualityCheckResults +=
            @{
                'Test'         = 'Module Tests'
                'Files Tested' = $ModulesToTest.Count
                'Total'        = ($moduleResults.TotalCount - $moduleResults.NotRunCount)
                'Passed'       = $moduleResults.PassedCount
                'Failed'       = $moduleResults.FailedCount
                'Skipped'      = $moduleResults.SkippedCount
            }
            $filesTested += $ModulesToTest.Count
            $total += ($moduleResults.TotalCount - $moduleResults.NotRunCount)
            $passed += $moduleResults.PassedCount
            $failed += $moduleResults.FailedCount
            $skipped += $moduleResults.SkippedCount
        }

        if ($null -ne $extractionResults) {
            $qualityCheckResults +=
            @{
                'Test'         = 'Extracting functions'
                'Files Tested' = $ModulesToTest.Count
                'Total'        = ($extractionResults.TotalCount - $extractionResults.NotRunCount)
                'Passed'       = $extractionResults.PassedCount
                'Failed'       = $extractionResults.FailedCount
                'Skipped'      = $extractionResults.SkippedCount
            }
            $total += ($extractionResults.TotalCount - $extractionResults.NotRunCount)
            $passed += $extractionResults.PassedCount
            $failed += $extractionResults.FailedCount
            $skipped += $extractionResults.SkippedCount
        }

        if ($null -ne $extractedScriptResults) {
            $qualityCheckResults +=
            @{
                'Test'         = 'Extracted function script tests'
                'Files Tested' = $extractedScriptsToTest.Count
                'Total'        = ($extractedScriptResults.TotalCount - $extractedScriptResults.NotRunCount)
                'Passed'       = $extractedScriptResults.PassedCount
                'Failed'       = $extractedScriptResults.FailedCount
                'Skipped'      = $extractedScriptResults.SkippedCount
            }
            $filesTested += $extractedScriptsToTest.Count
            $total += ($extractedScriptResults.TotalCount - $extractedScriptResults.NotRunCount)
            $passed += $extractedScriptResults.PassedCount
            $failed += $extractedScriptResults.FailedCount
            $skipped += $extractedScriptResults.SkippedCount
        }

        if ($null -ne $scriptResults) {
            $qualityCheckResults +=
            @{
                'Test'         = 'Script Tests'
                'Files Tested' = $scriptsToTest.Count
                'Total'        = ($scriptResults.TotalCount - $scriptResults.NotRunCount)
                'Passed'       = $scriptResults.PassedCount
                'Failed'       = $scriptResults.FailedCount
                'Skipped'      = $scriptResults.SkippedCount
            }
            $filesTested += $scriptsToTest.Count
            $total += ($scriptResults.TotalCount - $scriptResults.NotRunCount)
            $passed += $scriptResults.PassedCount
            $failed += $scriptResults.FailedCount
            $skipped += $scriptResults.SkippedCount
        }

        $qualityCheckResults +=
        @{
            'Test'         = 'Total'
            'Files Tested' = $filesTested
            'Total'        = $total
            'Passed'       = $passed
            'Failed'       = $failed
            'Skipped'      = $skipped
        }

        # This works on PS5 and PS7
        $qualityCheckResults | ForEach-Object {
            [PSCustomObject]@{
                'Test'         = $_.Test
                'Files Tested' = $_.'Files Tested'
                'Total'        = $_.total
                'Passed'       = $_.passed
                'Failed'       = $_.failed
                'Skipped'      = $_.skipped
            }
        } | Format-Table -AutoSize

        # This works on PS7 not on PS5
        # $qualityCheckResults | Select-Object Name, 'Files Tested', Total, Passed, Failed, Skipped | Format-Table -AutoSize

    }

    if ($PSBoundParameters.ContainsKey('ExportCheckResults')) {

        $projectResults | Export-Clixml -Path 'projectResults.xml'
        $moduleResults | Export-Clixml -Path 'moduleResults.xml'
        $extractionResults | Export-Clixml -Path 'extractionResults.xml'
        $scriptResults | Export-Clixml -Path 'scriptsToTest.xml'
        $extractedScriptResults | Export-Clixml -Path 'extractedScriptResults.xml'

    }

    if ($PSBoundParameters.ContainsKey('Passthru')) {

        if ($PesterConfiguration.Run.PassThru.Value -eq $true) {

            $resultObject = @{
                'project'         = $projectResults
                'module'          = $moduleResults
                'extraction'      = $extractionResults
                'script'          = $scriptResults
                'extractedscript' = $extractedScriptResults
            }

            return $resultObject

        } else {
            Write-Error 'Unable to pass back result objects. Passthru not enabled in Pester Configuration object'
        }

    }

}
#EndRegion '.\Public\Invoke-PSQualityCheck.ps1' 641