skills/changed-file-validation/scripts/Invoke-ChangedFileValidationWorker.ps1

#Requires -Version 5.1

<#
    .SYNOPSIS
        Runs the PowerShell checks for one snapshot inside an owned child process.
 
    .DESCRIPTION
        Not a user entry point. Invoke-ChangedFilePowerShellCheck starts this
        script in a separate process so that parsing and static analysis carry a
        wall-clock bound and can be stopped, and reads the single JSON document
        it writes to standard output.
 
        The request arrives on standard input rather than on a command line, and
        the file to check is named relative to the working directory the parent
        set, so no project text is ever composed into an argument.
 
        The file is read, never run: nothing here dot-sources, imports, or
        invokes the snapshot, and PSScriptAnalyzer is given an inline settings
        hashtable so it neither discovers a project settings file nor loads a
        custom rule module.
 
    .OUTPUTS
        System.String
#>

[CmdletBinding()]
param ()

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

function Write-WorkerReport
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [hashtable]$Report
    )

    [Console]::Out.Write(($Report | ConvertTo-Json -Depth 8 -Compress))
    [Console]::Out.Flush()
}

function New-WorkerDiagnostic
{
    [CmdletBinding()]
    [OutputType([hashtable])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions',
        '',
        Justification = 'Builds an in-memory diagnostic record and changes nothing.'
    )]
    param
    (
        [Parameter()]
        [int]$Line = 0,

        [Parameter()]
        [int]$Column = 0,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Severity,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$RuleId,

        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]$Message
    )

    return @{
        line = $Line
        column = $Column
        severity = $Severity
        ruleId = $RuleId
        message = $Message
    }
}

$request = [Console]::In.ReadToEnd() | ConvertFrom-Json
$fileName = [string]$request.fileName
$failOnSeverity = [string]$request.failOnSeverity
$maxDiagnostic = [int]$request.maxDiagnostic

$fullPath = [IO.Path]::Combine((Get-Location).ProviderPath, $fileName)

$parseError = $null
$null = [Management.Automation.Language.Parser]::ParseFile($fullPath, [ref] $null, [ref] $parseError)
$found = @($parseError)

$parse = @{
    errorCount = $found.Count
    diagnostics = @(
        foreach ($item in ($found | Select-Object -First $maxDiagnostic))
        {
            New-WorkerDiagnostic `
                -Line $item.Extent.StartLineNumber `
                -Column $item.Extent.StartColumnNumber `
                -Severity 'Error' `
                -RuleId ([string]$item.ErrorId) `
                -Message ([string]$item.Message)
        }
    )
}

$module = Get-Module -Name PSScriptAnalyzer -ListAvailable -ErrorAction SilentlyContinue |
    Sort-Object -Property Version -Descending |
    Select-Object -First 1

$analyzer = if ($null -eq $module)
{
    @{ available = $false; reason = 'ModuleNotAvailable'; version = ''; failingCount = 0; diagnostics = @() }
}
else
{
    try
    {
        Import-Module -Name PSScriptAnalyzer -RequiredVersion $module.Version -ErrorAction Stop

        # -Path accepts wildcards, so even a generated literal name is escaped.
        # -Settings takes an inline hashtable, which is what stops the analyzer
        # discovering a PSScriptAnalyzerSettings.psd1 anywhere near the input.
        $finding = @(
            Invoke-ScriptAnalyzer `
                -Path ([Management.Automation.WildcardPattern]::Escape($fullPath)) `
                -Settings @{ Severity = @('Error', 'Warning') } `
                -ErrorAction Stop
        )

        $failing = @(
            $finding | Where-Object -FilterScript {
                [string]$_.Severity -eq 'Error' -or
                ($failOnSeverity -eq 'Warning' -and [string]$_.Severity -eq 'Warning')
            }
        )

        @{
            available = $true
            reason = ''
            version = $module.Version.ToString()
            failingCount = $failing.Count
            diagnostics = @(
                foreach ($item in ($finding | Select-Object -First $maxDiagnostic))
                {
                    New-WorkerDiagnostic `
                        -Line ([int]$item.Line) `
                        -Column ([int]$item.Column) `
                        -Severity ([string]$item.Severity) `
                        -RuleId ([string]$item.RuleName) `
                        -Message ([string]$item.Message)
                }
            )
        }
    }
    catch
    {
        @{
            available = $false
            reason = 'InvocationFailed'
            version = $module.Version.ToString()
            failingCount = 0
            diagnostics = @()
        }
    }
}

Write-WorkerReport -Report @{ parse = $parse; analyzer = $analyzer }
exit 0