Rules/PSAvoidImmediatePipelineBuffering.psm1

Set-StrictMode -Version Latest

$script:RuleName = 'PSAvoidImmediatePipelineBuffering'
$script:RuleMessage = 'Avoid assigning command or pipeline output to a variable only to enumerate it immediately with foreach in pipeline-facing commands. Keep the producer in the pipeline or enumerate it directly to preserve streaming shape.'

function Measure-PSAvoidImmediatePipelineBuffering {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.Language.ScriptBlockAst] $ScriptBlockAst
    )

    $functions = $ScriptBlockAst.FindAll({
        param($Ast)
        $Ast -is [System.Management.Automation.Language.FunctionDefinitionAst]
    }, $true)

    foreach ($functionAst in $functions) {
        if (-not (Test-IsPipelineFacingCommand -FunctionAst $functionAst)) {
            continue
        }

        $assignments = @(Get-ImmediateBufferedAssignments -FunctionAst $functionAst)
        foreach ($assignment in $assignments) {
            [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]::new(
                $script:RuleMessage,
                $assignment.Extent,
                $script:RuleName,
                [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticSeverity]::Warning,
                $null,
                $null,
                $null
            )
        }
    }
}

function Test-IsPipelineFacingCommand {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst] $FunctionAst
    )

    if (-not $FunctionAst.Body.ParamBlock) {
        return $false
    }

    $hasCmdletBinding = $FunctionAst.Body.ParamBlock.Attributes | Where-Object {
        $_.TypeName.GetReflectionType().FullName -eq 'System.Management.Automation.CmdletBindingAttribute'
    }

    if (-not $hasCmdletBinding) {
        return $false
    }

    return $FunctionAst.Body.ProcessBlock -ne $null
}

function Get-ImmediateBufferedAssignments {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst] $FunctionAst
    )

    $candidates = $FunctionAst.FindAll({
        param($Ast)
        $Ast -is [System.Management.Automation.Language.AssignmentStatementAst] -and
        $Ast.Left -is [System.Management.Automation.Language.VariableExpressionAst]
    }, $true)

    foreach ($assignment in $candidates) {
        if (-not (Test-HasProducedPipelineRightHandSide -AssignmentAst $assignment)) {
            continue
        }

        if (-not (Test-IsImmediateForeachEnumeration -FunctionAst $FunctionAst -AssignmentAst $assignment)) {
            continue
        }

        $assignment
    }
}

function Test-HasProducedPipelineRightHandSide {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.Language.AssignmentStatementAst] $AssignmentAst
    )

    $pipelines = $AssignmentAst.Right.FindAll({
        param($Ast)
        $Ast -is [System.Management.Automation.Language.PipelineAst]
    }, $true)

    foreach ($pipeline in $pipelines) {
        foreach ($element in $pipeline.PipelineElements) {
            if ($element -is [System.Management.Automation.Language.CommandAst]) {
                return $true
            }
        }
    }

    return $false
}

function Test-IsImmediateForeachEnumeration {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst] $FunctionAst,

        [Parameter(Mandatory)]
        [System.Management.Automation.Language.AssignmentStatementAst] $AssignmentAst
    )

    $parentBlock = $AssignmentAst.Parent
    if ($null -eq $parentBlock -or -not ($parentBlock.PSObject.Properties.Name -contains 'Statements')) {
        return $false
    }

    $statementIndex = -1
    for ($i = 0; $i -lt $parentBlock.Statements.Count; $i++) {
        if ($parentBlock.Statements[$i] -eq $AssignmentAst) {
            $statementIndex = $i
            break
        }
    }

    if ($statementIndex -lt 0 -or $statementIndex -ge ($parentBlock.Statements.Count - 1)) {
        return $false
    }

    $nextStatement = $parentBlock.Statements[$statementIndex + 1]
    if ($nextStatement -isnot [System.Management.Automation.Language.ForEachStatementAst]) {
        return $false
    }

    $assignedVariableName = $AssignmentAst.Left.VariablePath.UserPath
    $conditionPipeline = $nextStatement.Condition
    if ($conditionPipeline -isnot [System.Management.Automation.Language.PipelineAst] -or
        $conditionPipeline.PipelineElements.Count -ne 1) {
        return $false
    }

    $element = $conditionPipeline.PipelineElements[0]
    if ($element -isnot [System.Management.Automation.Language.CommandExpressionAst]) {
        return $false
    }

    $expression = $element.Expression
    if ($expression -isnot [System.Management.Automation.Language.VariableExpressionAst]) {
        return $false
    }

    return $expression.VariablePath.UserPath -eq $assignedVariableName
}

Export-ModuleMember -Function Measure-PSAvoidImmediatePipelineBuffering