Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Parameter Binder bug with Advanced Functions and ValueFromRemainingArguments #2038

Merged

Conversation

dlwyatt
Copy link
Contributor

@dlwyatt dlwyatt commented Aug 23, 2016

Resolves issues #2035

In the current code, two calls to BindParameter are potentially made, and the assumption was that if a caller had specified Set-ClusterOwnerNode foo,bar instead of Set-ClusterOwnerNode foo bar, the first call would fail.

This was not the case with Advanced Functions, so the logic which was supposed to cover that case in HandleRemainingArguments() wasn't being triggered. This update unwraps a single-element list first, rather than requiring a failed call to BindParameter before that happens.

As a result, Write-Object no longer needs its own foreach loop to compensate for being sent a List<Object> from HandleRemainingArguments().

@lzybkr
Copy link
Member

lzybkr commented Aug 23, 2016

I think this is a bucket 2 breaking change, so maybe we need an RFC.

@daxian-dbw - can you review?

@dlwyatt
Copy link
Contributor Author

dlwyatt commented Aug 23, 2016

If someone's depending on this difference in behavior between Do-Something 1 2 3 and Do-Something 1,2,3, I want to give that person a stern talking-to. :)

@TravisEz13 TravisEz13 changed the title Fixes #2035 (Parameter Binder bug with Advanced Functions and ValueFromRemainingArguments) Fix Parameter Binder bug with Advanced Functions and ValueFromRemainingArguments Aug 23, 2016
@lzybkr lzybkr added the WG-Engine core PowerShell engine, interpreter, and runtime label Aug 23, 2016
WriteObject(inputObject, enumerate);
}

WriteObject(_inputObjects, enumerate);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't been changed even if the fix to parameter binder is accepted. it will break the following scenario:

Write-Output aa,bb cc,dd

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that's tricky. In fact I'd say it's been a bug in Write-Output all along.

Write-Output aa,bb cc,dd

# is equivalent to

Write-Output -InputObject @( @('aa', 'bb'), @('cc', 'dd') )

Either way, InputObject is a 2-element array, and each element also happens to be a 2-element array. What I would expect to happen here is what happens after this PR: You send two objects down the pipeline, both of which are 2-element arrays. If you added the -NoEnumerate switch, then you would send one object down the pipeline, which is a 2-element array containing two other 2-element arrays. (In fact, with the -NoEnumerate switch, you should only ever send one object, an array, down the pipeline, which is sort of the point.)

Today, Write-Output aa,bb cc,dd instead sends 4 strings down the pipeline. It enumerates two levels deep instead of one. If you use the -NoEnumerate switch on that command today, you get two 2-element arrays going down the pipeline (what I'd expect to see without -NoEnumerate).

Without this change, several other Write-Output tests failed when I ran Start-PSPester. With this change, everything was green, but apparently that just means that the test coverage isn't good enough right now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are the results of Write-Output.Tests.ps1 with the binder fix but without the change to Write-Output:

Describing Write-Output DRT Unit Tests
 [-] Simple Write Object Test 1.28s
   Expected: {4}
   But was:  {6}
   at line: 5 in C:\Users\dlwya_000\Documents\GitHub\powershell\test\powershell\Modules\Microsoft.PowerShell.Utility\Write-Output.Tests.ps1
   5:         $results.Length | Should Be $objectWritten.Length
 [-] Works with NoEnumerate switch 144ms
   Expected: {1 2.2 System.Object[] abc}
   But was:  {1}
   at line: 19 in C:\Users\dlwya_000\Documents\GitHub\powershell\test\powershell\Modules\Microsoft.PowerShell.Utility\Write-Output.Tests.ps1
   19:         Write-Output $objectWritten -NoEnumerate 6>&1 | Should be  '1 2.2 System.Object[] abc'
Describing Write-Output
   Context Input Tests
    [+] Should allow piped input 335ms
    [+] Should write output to the output stream when using piped input 23ms
    [+] Should use inputobject switch 25ms
    [+] Should write output to the output stream when using inputobject switch 38ms
    [+] Should be able to write to a variable 21ms
   Context Pipeline Command Tests
    [+] Should send object to the next command in the pipeline 69ms
    [+] Should have the same result between inputobject switch and piped input 23ms
   Context Enumerate Objects
    [+] Should see individual objects when not using the NoEnumerate switch 62ms
    [-] Should be able to treat a collection as a single object using the NoEnumerate switch 32ms
      Expected: {1}
      But was:  {3}
      at line: 71 in C:\Users\dlwya_000\Documents\GitHub\powershell\test\powershell\Modules\Microsoft.PowerShell.Utility\Write-Output.Tests.ps1
      71:           $singleCollection | Should Be 1
Tests completed in 2.06s
Passed: 8 Failed: 3 Skipped: 0 Pending: 0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man, what a mess. Here's some of Write-Output's behavior today:

 write-output -NoEnumerate a b c | % { $_.GetType().FullName; $_.Count; $_ -join '/' }
System.String
1
a
System.String
1
b
System.String
1
c

 write-output -NoEnumerate a,b,c | % { $_.GetType().FullName; $_.Count; $_ -join '/' }
System.Object[]
3
a/b/c

This is a symptom of the flawed binding logic I changed (assuming the first call to BindParameter would fail when the a,b,c syntax was used), and the workaround that was created for it in Write-Output. Whatever the correct behavior is, there should never be a difference between these two calls when ValueFromRemainingArguments is in effect:

Do-Something a b c
Do-Something a,b,c

The values bound to the parameter should be identical in both cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's possible to fix the binding logic without somehow changing (breaking or fixing, depending on your point of view) the behavior of Write-Output. The whole point of the binding fix is to ensure consistent behavior between those two syntax options I listed, and it happens before the cmdlet ever gets to look at the values.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Write-Output a,b,c
This is a symptom of the flawed binding logic I changed (assuming the first call to BindParameter would fail when the a,b,c syntax was used)

in this case, the first call to BindParameter is successful, because the parameter type of InputObject is PSObject[], and type conversion from list<object[]> to it would be successful.

write-output -NoEnumerate a b c | % { $_.GetType().FullName; $_.Count; $_ -join '/' }
System.String
1
a
System.String
1
b
System.String
1
c

write-output -NoEnumerate a,b,c | % { $_.GetType().FullName; $_.Count; $_ -join '/' }
System.Object[]
3
a/b/c

IMHO, this behavior of Write-Output is pretty intuitive -- when -NoEnumerate is specified, give me back what ever items I gave you. In case of a b c, I gave you 3 items, so 3 items should be returned by Write-Output; in case of a,b,c, I gave you 1 item, which is an array, so the same item should be returned back.

However, it gets confusing when -InputObject is explicitly specified:

PS D:\> Write-Output -InputObject a,b,c -NoEnumerate | % { $_.GetType().FullName; $_.Count; $_ -join '/' }
System.String
1
a
System.String
1
b
System.String
1
c
PS D:\> Write-Output a,b,c -NoEnumerate | % { $_.GetType().FullName; $_.Count; $_ -join '/' }
System.Object[]
3
a/b/c

This is the part I don't like about ValueFromRemainingArgument, but changing it would definitely break something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case of a b c, I gave you 3 items, so 3 items should be returned by Write-Output
in case of a,b,c, I gave you 1 item,

I disagree with that. ValueFromRemainingArguments is just syntax sugar, similar to a params parameter in C#. You gave 3 objects in both versions of the command.

@daxian-dbw
Copy link
Member

as shown by the discussion about Write-Output, the change to HandleRemainingArguments in this PR would be a breaking change.
another example would be when an array of string can be converted to a specific element type of the parameter type:

class dummy {
    [object] $Path
    [object] $Name
    dummy ([object[]] $a) {
        $this.Path = $a[0]
        $this.Name = $a[1]
    }
    [string] ToString() {
        return ($this.Path + " -- " + $this.Name)
    }
}


function Test-Attribute
{
    param(
         [string[]]
         [Parameter(Position=0)]
         $Path,

         [dummy[]]
         [Parameter(ValueFromRemainingArguments)]
         $Extra
         )


     $Extra.Count;
     for ($i = 0; $i -lt $Extra.Count; $i++)
     {
        "${i}: $($Extra[$i].GetType().FullName) $($Extra[$i])"
     }
}

PS D:\> Test-Attribute root aa,bb
1
0: dummy aa -- bb
PS D:\>
PS D:\> Test-Attribute root aa bb
Test-Attribute : Cannot process argument transformation on parameter 'Extra'. Cannot convert value
"System.Collections.Generic.List`1[System.Object]" to type "dummy[]". Error: "Cannot convert the "aa" value of type
"System.String" to type "dummy"."
At line:1 char:1
+ Test-Attribute root aa bb
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [Test-Attribute], ParameterBindingArgumentTransformationException
    + FullyQualifiedErrorId : ParameterArgumentTransformationError,Test-Attribute

Another proposal of the fix:
If the parameter is declared with ValueFromRemaningArgument and Position, and valueFromRemainingArguments.Count == 1, then we use the specified argument directly instead of wrapping it in a List.

When Position is specified, I assume the author would like do-something aa to be interpreted as do-something -param aa instead of treating aa as remaining arguments.

This won't solve the problem to parameters with ValueFromRemaningArgument but without Position, and also would require Write-Object to be changed -- remove Position=0 from the parameter declaration of InputObject. So this would also be a breaking change.

Fixing parameter binder bugs is very tricky. It would be so easy to break existing behavior.

@dlwyatt
Copy link
Contributor Author

dlwyatt commented Aug 24, 2016

The more I play with this, the more I think it's the existing behavior that's broken. I'd advise people not to declare ValueFromRemainingArguments parameters at all, in the current form.

@daxian-dbw
Copy link
Member

Let me get more data about the usage of ValueFromRemainingArguments and Write-Output. Will reply back here.

@jpsnover
Copy link
Contributor

jpsnover commented Aug 28, 2016

IMHO, this behavior of Write-Output is pretty intuitive -- when -NoEnumerate is specified, give me back what ever items I gave you. In case of a b c, I gave you 3 items, so 3 items should be returned by Write-Output; in case of a,b,c, I gave you 1 item, which is an array, so the same item should be returned back.

@dlwyatt I'm not a good person to explain the parsing (maybe @BrucePay can weigh in) but in this example, the parser views this as 1 parameter - an array with 3 values.

Or in this example:

Do-Something 1 2 3
This has 3 parameters with one value each.

Do-Something 1,2,3
This has 1 parameter with 3 values.

I don't see any value in breaking existing semantics but we could consider adding a new attribute [CollapseAllArgumentsIntoASingleArray](name TBD)
That seems like a bad thing because cmdlets with positional parameters are, and need to be, precise.
So a change like this would yield a unpredictable UX.

@dlwyatt
Copy link
Contributor Author

dlwyatt commented Aug 28, 2016

@jpsnover : That's exactly what ValueFromRemainingArguments is supposed to do already. It's the equivalent of declaring params string[] someParam in C#. There are just some implementation quirks that cause it to not behave the same way when one syntax or the other are used.

@lzybkr
Copy link
Member

lzybkr commented Sep 22, 2016

We have discussed this and would like more data about potential breakage if we took the change.
We'll leave this open until somebody (likely us) do that work.

@dlwyatt dlwyatt closed this Sep 26, 2016
@lzybkr lzybkr reopened this Nov 2, 2016
@mirichmo mirichmo added the Breaking-Change breaking change that may affect users label Nov 3, 2016
@PowerShellTeam PowerShellTeam added the Review - Needed The PR is being reviewed label Nov 22, 2016
@HemantMahawar HemantMahawar added Review - Needed The PR is being reviewed and removed Review - Needed The PR is being reviewed Review-Needed labels Dec 1, 2016
@dlwyatt dlwyatt force-pushed the ValueFromRemainingArgumentsFix branch from 6becaf0 to 578bbfd Compare May 25, 2017 18:25
else
{
$tempDir = '/tmp'
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use '$TestDrive' instead of '$env:temp' and '/tmp'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's already a comment explaining why that doesn't work.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Please clarify - Pester will fail to clean up or Pester will fail all test batch?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use explicit assembly?
Can we use:

Add-Type -TypeDefinition $a -PassThru | ForEach-Object {$_.assembly} | Import-module -Force 

@dlwyatt
Copy link
Contributor Author

dlwyatt commented May 30, 2017

When you import the temporary module, the assembly is in use until PowerShell exits. That's why I don't create it in TestDrive:, and why I use the same file name every time (so we're not littering the test system with multiple dlls).

@iSazonov
Copy link
Collaborator

@dlwyatt Thanks for clafify!
We use temporary modules in several tests and we never cared about unloading them. That's why I'm surprised we're doing this here.

@dlwyatt
Copy link
Contributor Author

dlwyatt commented Oct 11, 2017

Just rebased. Will check back in a bit to make sure the tests are still passing.

@daxian-dbw
Copy link
Member

@dlwyatt Thank you! This will be the last rebase for sure 😄

@daxian-dbw daxian-dbw merged commit 94a71b0 into PowerShell:master Oct 12, 2017
@iSazonov
Copy link
Collaborator

@daxian-dbw Should we add Docs-Needed and remove Review-Needed?

@dlwyatt dlwyatt deleted the ValueFromRemainingArgumentsFix branch October 12, 2017 13:05
@daxian-dbw daxian-dbw added Documentation Needed in this repo Documentation is needed in this repo and removed Review - Needed The PR is being reviewed labels Oct 12, 2017
// Set-ClusterOwnerNode foo bar
// Set-ClusterOwnerNode foo,bar
// we unwrap our List, but only if there is a single argument of type object[].
if (valueFromRemainingArguments.Count == 1 && valueFromRemainingArguments[0] is object[])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be valueFromRemainingArguments[0]?.GetType() == typeof(object[]) or valueFromRemainingArguments[0] is Array?
Given array covariance in .NET, any array of reference type (like string[]) will pass is object[] check, but not array of value type (like int[]). Should it be only object[] or all arrays instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PetSerAl - good question, I think the intent was all arrays, though object[] in this context is nearly the same thing.

@dlwyatt , @daxian-dbw - thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you're right. Most of the time, PowerShell's building object[] arrays from arguments at that point, but if you explicitly make a variable of another type and pass it in, it doesn't get unwrapped.

Rather than is Array, though, I'd recommend using LanguagePrimitives.GetEnumerable(valueFromRemainingArguments[0]) . That will cover other types of collections using the same rules that you'd expect from PowerShell (not treating strings / dictionaries as collections, etc.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm putting together new tests for this and will follow up with a second PR

mklement0 referenced this pull request in mklement0/PowerShell Mar 22, 2018
@joeyaiello joeyaiello removed Documentation Needed in this repo Documentation is needed in this repo labels Oct 15, 2018
iSazonov pushed a commit that referenced this pull request Mar 13, 2019
Fix is to preserve input collection type in output.
The regression was caused by #2038
TravisEz13 pushed a commit that referenced this pull request Mar 13, 2019
Fix is to preserve input collection type in output.
The regression was caused by #2038
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Breaking-Change breaking change that may affect users Committee-Reviewed PS-Committee has reviewed this and made a decision WG-Engine core PowerShell engine, interpreter, and runtime
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet