Rule request `AvoidRedundantValueFromPipelineByPropertyName`
Opened this issue · 2 comments
As concluded here PowerShell/PowerShell#25953
The ValueFromPipelineByPropertyName parameter attribute should not be used with the ValueFromPipeline parameter attribute because it would not have any effect (the ValueFromPipeline attribute overrules the ValueFromPipelineByPropertyName attribute).
This is only when the parameter is typed fairly generically though right? [object] or [PSCustomObject].
Pipeline parameter binding is attempted in a specific order.
If the parameter accepts pipeline input by value (ValueFromPipeline) and by property name (ValueFromPipelineByPropertyName)...
-
ValueFromPipeline, no coercion: If the pipeline object's type and parameter's type match then we have a winner.
-
ValueFromPipelineByPropertyName, no coercion: If the pipeline object has a matching property name and the types match then we have a winner.
-
ValueFromPipeline, with coercion: If PowerShell can coerce the pipeline object into the parameters type, then we have a winner.
-
ValueFromPipelineByPropertyName, with coercion: If the pipeline object has a matching property name and PowerShell can coerce the pipeline object into the parameters type, then we have a winner.
-
Otherwise Fail:, binding fails with a type conversion error.
Cannot process argument transformation on parameter 'X'. Cannot convert value "Y" to type "Type". Error: "ErrorMessage."
The below works as you expect.
function Test-Function {
param (
[Parameter(
ValueFromPipeline,
ValueFromPipelineByPropertyName
)]
[string]
$Param1,
[Parameter(
ValueFromPipelineByPropertyName
)]
[string]
$Param2
)
process {
Write-Host "Param1: '$($Param1)'"
Write-Host "Param2: '$($Param2)'"
}
}
[PSCustomObject]@{
Param1 = 'string1'
Param2 = 'string2'
} | Test-FunctionParam1: 'string1'
Param2: 'string2'Tracing the pipeline parameter binding of your repro case:
function test {
param (
[Parameter(ValueFromPipelineByPropertyName)]
$name,
[Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
$value
)
Write-Host "$name = $value"
}
Trace-Command ParameterBinding {
[PSCustomObject]@{ name = 1; value = 2 } | Test
} -PSHostBIND PIPELINE object to parameters: [test]
PIPELINE object TYPE = [System.Management.Automation.PSCustomObject]
RESTORING pipeline parameter's original values
Parameter [value] PIPELINE INPUT ValueFromPipeline NO COERCION
BIND arg [@{name=1; value=2}] to parameter [value]
BIND arg [@{name=1; value=2}] to param [value] SUCCESSFUL
Parameter [name] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
BIND arg [1] to parameter [name]
BIND arg [1] to param [name] SUCCESSFUL
MANDATORY PARAMETER CHECK on cmdlet [test]
CALLING ProcessRecordThe ValueFromPipeline binding from value is evaluated first and succeeds - so that wins and ValueFromPipelineByPropertyName for value is ignored.
Repeating this after giving both parameters [int] types results in:
BIND PIPELINE object to parameters: [test]
PIPELINE object TYPE = [System.Management.Automation.PSCustomObject]
RESTORING pipeline parameter's original values
Parameter [value] PIPELINE INPUT ValueFromPipeline NO COERCION
BIND arg [@{name=1; value=2}] to parameter [value]
Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
result returned from DATA GENERATION: @{name=1; value=2}
BIND arg [@{name=1; value=2}] to param [value] SKIPPED
Parameter [value] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
BIND arg [2] to parameter [value]
Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
result returned from DATA GENERATION: 2
BIND arg [2] to param [value] SUCCESSFUL
Parameter [name] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
BIND arg [1] to parameter [name]
Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
result returned from DATA GENERATION: 1
BIND arg [1] to param [name] SUCCESSFUL
MANDATORY PARAMETER CHECK on cmdlet [test]
CALLING ProcessRecordValueFromPipeline without coercion fails (since @{name=1; value=2} is not an [int]). It then evaluates ValueFromPipelineByPropertyName without coercion, which succeeds as the object as a matching property name and is an [int].
Repeating but with input:
[PSCustomObject]@{ name = 1; value = "2" } | TestGives
BIND PIPELINE object to parameters: [test]
PIPELINE object TYPE = [System.Management.Automation.PSCustomObject]
RESTORING pipeline parameter's original values
Parameter [value] PIPELINE INPUT ValueFromPipeline NO COERCION
BIND arg [@{name=1; value=2}] to parameter [value]
Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
result returned from DATA GENERATION: @{name=1; value=2}
BIND arg [@{name=1; value=2}] to param [value] SKIPPED
Parameter [value] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
BIND arg [2] to parameter [value]
Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
result returned from DATA GENERATION: 2
BIND arg [2] to param [value] SKIPPED
Parameter [name] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
BIND arg [1] to parameter [name]
Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
result returned from DATA GENERATION: 1
BIND arg [1] to param [name] SUCCESSFUL
Parameter [value] PIPELINE INPUT ValueFromPipeline WITH COERCION
BIND arg [@{name=1; value=2}] to parameter [value]
Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
ERROR: DATA GENERATION: Cannot convert the "@{name=1; value=2}" value of type "System.Management.Automation.PSCustomObject" to type "System.Int32".
Parameter [value] PIPELINE INPUT ValueFromPipelineByPropertyName WITH COERCION
BIND arg [2] to parameter [value]
Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
result returned from DATA GENERATION: 2
COERCE arg to [System.Int32]
Parameter and arg types the same, no coercion is needed.
BIND arg [2] to param [value] SUCCESSFUL
MANDATORY PARAMETER CHECK on cmdlet [test]
CALLING ProcessRecordYou see all 4 binding attempts for Value here. Ultimately ValueFromPipelineByPropertyName with coercion wins out as it can successfully convert "2" into 2.
If there was going to be a rule to warn about this, it would be when:
-
Both
ValueFromPipelineByPropertyNameandValueFromPipelineare present on a parameter and both true (either explicitly or implicitly) -
The parameter's static type is known to be
System.ObjectorSystem.Management.Automation.PSObject(and potentially evenIEnumerablevariants of those. i.e.System.Object[], but would need to check how PowerShell handles those).ParameterAsthas a propertyStaticType.
- The parameter's static type is known to be
System.ObjectorSystem.Management.Automation.PSObject
Good point, I completely missed that condition, which makes sense as you can't pull a property from a scalar, or ...
A string has properties... Ouch, that looks buggy, take the following two function examples:
function Test-Length {
param (
[Parameter(
ValueFromPipeline,
ValueFromPipelineByPropertyName
)]
[int]
$Length
)
process {
Write-Host "Length: '$($Length)'"
}
}
Read-Host 'Input a length (e.g.: 42)' | Test-Lengthfunction Test-MyLength {
param (
[Parameter(
ValueFromPipeline,
ValueFromPipelineByPropertyName
)]
[int]
$MyLength
)
process {
Write-Host "MyLength: '$($MyLength)'"
}
}
Read-Host 'Input a length (e.g.: 42)' | Test-MyLengthYou might expect the same functionality here, but that is not the case...
So:
- The parameter's static type is known to be
System.ObjectorSystem.Management.Automation.PSObject(and potentially evenIEnumerablevariants of those. i.e.System.Object[], but would need to check how PowerShell handles those).
Might generally be correct the actually situation appears some like: The parameter's static type does have any property with the same name and type as the concerned parameter...
(I will report this back to the initial PowerShell issue)