Rules/PSAvoidAmbiguousPipelineInput.psm1

Set-StrictMode -Version Latest

$script:RuleName = 'PSAvoidAmbiguousPipelineInput'
$script:RuleMessage = 'Avoid exposing multiple ValueFromPipeline parameters on the same pipeline-facing command. Keep one clear pipeline-by-value intake path so pipeline binding stays predictable.'

function Measure-PSAvoidAmbiguousPipelineInput {
    [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
        }

        $pipelineByValueParameters = @(Get-PipelineByValueParameters -FunctionAst $functionAst)
        if ($pipelineByValueParameters.Count -le 1) {
            continue
        }

        [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]::new(
            $script:RuleMessage,
            $functionAst.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-PipelineByValueParameters {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.Language.FunctionDefinitionAst] $FunctionAst
    )

    @(
        $FunctionAst.Body.ParamBlock.Parameters |
            Where-Object { Test-ParameterAcceptsPipelineByValue -ParameterAst $_ }
    )
}

function Test-ParameterAcceptsPipelineByValue {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.Language.ParameterAst] $ParameterAst
    )

    foreach ($attribute in $ParameterAst.Attributes) {
        if ($attribute.TypeName.FullName -ne 'Parameter') {
            continue
        }

        foreach ($namedArgument in $attribute.NamedArguments) {
            if ($namedArgument.ArgumentName -eq 'ValueFromPipeline') {
                return $true
            }
        }
    }

    return $false
}

Export-ModuleMember -Function Measure-PSAvoidAmbiguousPipelineInput