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

Misleading or missing information on member-access enumeration #11618

Closed
3 tasks done
surfingoldelephant opened this issue Dec 31, 2024 · 0 comments · Fixed by #11627
Closed
3 tasks done

Misleading or missing information on member-access enumeration #11618

surfingoldelephant opened this issue Dec 31, 2024 · 0 comments · Fixed by #11627
Assignees
Labels
area-about Area - About_ topics issue-doc-bug Issue - error in documentation

Comments

@surfingoldelephant
Copy link
Contributor

surfingoldelephant commented Dec 31, 2024

Prerequisites

  • Existing Issue: Search the existing issues for this repository. If there is an issue that fits your needs do not file a new one. Subscribe, react, or comment on that issue instead.
  • Descriptive Title: Write the title for this issue as a short synopsis. If possible, provide context. For example, "Typo in Get-Foo cmdlet" instead of "Typo."
  • Verify Version: If there is a mismatch between documentation and the behavior on your system, ensure that the version you are using is the same as the documentation. Check this box if they match or the issue you are reporting is not version specific.

Links

https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_member-access_enumeration

Summary

about_member-access_enumeration has misleading or missing information on:

  1. Support for non-IList-implementing collections.
  2. Member-access enumeration's equivalence to alternative approaches involving, e.g., ForEach-Object or ForEach().
  3. How member-access enumeration is propagated to nested collections.
  4. What happens to output from prior method calls when enumeration is terminated.
  5. How the presence of a custom object affects $null output.

Details

Item 1

  • Throughout the document, reference is made to "list collection", including a specific check for the IList interface.

    You can check if an object is a list collection by seeing whether its type implements the IList interface: [...]

  • However, collections that do not implement IList are still supported by member-access enumeration.

    $q = [Collections.Generic.Queue[object]]::new(('foo', 'bar'))
    $q -is [Collections.IList] # False
    $q.ToUpper()
    # FOO
    # BAR
  • If LanguagePrimitives.IsObjectEnumerable() reports $true (publicly available in PS v6+), member-access enumeration is supported.

    [Management.Automation.LanguagePrimitives]::IsObjectEnumerable($q)
    # True
    
    $expo = [Dynamic.ExpandoObject]::new()
    $expo.a = 'a'; $expo.b = 'b'
    [Management.Automation.LanguagePrimitives]::IsObjectEnumerable($expo)
    # True
    
    # Use member-access enumeration to retrieve the "Value" property of the KeyValuePair elements.
    # "Value" is not a property of the ExpandoObject itself.
    $expo.Value
    # a
    # b
    
    [Management.Automation.LanguagePrimitives]::IsObjectEnumerable(@{ a = 'b' })
    # False

Item 2

  • The following statement above the first Long description example is misleading.

    These [ForEach-Object/ForEach()/member-access enumeration] commands are functionally identical

  • In general, there are numerous differences, such as:
    • Input processing (in-memory collections vs object streaming).
    • Likelihood of member name collisions.
    • Wildly inconsistent behavior with dictionaries.
    • Output type.
    • Handling of missing non-method members.
    • Handling of a missing method or method error.
    • Member-access propagation to nested collections.
    • Member name collisions between the collection and its elements.
  • Regarding the Get-Service example specifically, there are not quite as many, but notable differences are nonetheless present (e.g., with member-access enumeration/ForEach(), Get-Service must run to completion, there are output differences if event* yields no results, etc).
  • It may be best to emphasize that member-access enumeration is purely a convenience feature and that due to its design, differences in behavior may be encountered when comparing it with alternative approaches.

Item 3

  • If the to-be-enumerated collection contains collections itself, member-access enumeration is applied to those collections (and so on, so forth). For example:

    # $a is an array containing two elements:
    #   A nested array -> nested array -> "bar", "baz"
    #   "foo"
    $a = (, (, ('bar', 'baz'))), 'foo'
    $a.ToUpper()
    # BAR
    # BAZ
    # FOO
    
    # The result is a flat array of three strings. 

Item 4

  • If enumeration is terminated, either from an object lacking an accessed method or from a method raising a terminating error, output from prior successful method calls is not returned.

    class Class1 { [object] Foo() { return 'Bar' } }
    class Class2 { [void] Foo() { throw 'Error' } }
    class Class3 {}
    
    # No issue; "Bar" output from both.
    ([Class1]::new(), [Class1]::new()).Foo()
    # Bar
    # Bar
    
    # On error.
    # No "Bar" output from Class1.
    ([Class1]::new(), [Class2]::new()).Foo()
    # Exception:
    # Line |
    #    2 |  class Class2 { [void] Foo() { throw 'Error' } }
    #      |                                ~~~~~~~~~~~~~
    #      | Error
    
    # On missing method.
    # No "Bar" output from Class1.
    ([Class1]::new(), [Class3]::new()).Foo()
    # InvalidOperation: Method invocation failed because [class3] does not contain a method named 'foo'.
  • This is one notable difference in behavior when compared with ForEach-Object. I think explicitly calling out that output may be lost with member-access enumeration is warranted, especially as ForEach-Object is already mentioned in the document.

    # "Bar" output from Class1.
    [Class1]::new(), [Class2]::new() | ForEach-Object -MemberName Foo
    # Bar
    # ForEach-Object: Exception calling "Foo" with "0" argument(s): "Error"

Item 5

  • If the collection of objects contains Management.Automation.PSCustomObject instances, unexpected $null values are returned if an accessed property is missing.

    $foo, $bar, $baz = [pscustomobject] @{ Foo = 'Foo' }, [pscustomobject] @{ Bar = 'Bar' },
                       [pscustomobject] @{ Baz = 'Baz' }
    
    # At least one object has a "Foo" property, so $null values should *not* be returned.
    # Yet a $null value is returned, one for each custom object without "Foo".
    ConvertTo-Json ($foo, $bar, $baz).Foo
    # [
    #   "Foo",
    #   null,
    #   null
    # ]
    
    ConvertTo-Json ((Get-Process -Id $PID), $foo).Name
    # [
    #   "pwsh",
    #   null
    # ]
    
    # No object has a "Foo" property, so a *single* $null value should be returned.
    # Yet multiple $null values are.
    ConvertTo-Json ($bar, $baz).Foo
    # [
    #   null,
    #   null
    # ]
  • This is a longstanding issue that dates back to Windows PowerShell. I believe it's worth calling out as the behavior is quite obscure.

Suggested Fix

In about_member-access_enumeration:

  1. Replace the term "list collection" with "collection" and use [Management.Automation.LanguagePrimitives]::IsObjectEnumerable() instead of checking for IList in the existing example to demonstrate what is/isn't considered a collection.
  2. In Long description:
    • Emphasize that member-access enumeration is purely a convenience feature. Behavior differences (some subtle) may be encountered when compared with alternative approaches. I suspect further detail is not within the scope of the document.
    • Perhaps mention ForEach-Object -MemberName and ForEach(propertyName)/ForEach(methodName), as syntactically, these are closer alternatives.
    • Mention that member-access enumeration can only operate on an in-memory collection, whereas ForEach-Object is intended for streaming/one-at-a-time processing.
  3. Add a new example showing how member-access enumeration is propagated to nested collections.
  4. Add to the existing warning in Long description that output from successful method calls is lost if enumeration is subsequently terminated due to a missing method/method error.
  5. Add a warning at the bottom of the document calling out how custom object inclusion in the enumerated collection affects $null output scenarios.
@surfingoldelephant surfingoldelephant added issue-doc-bug Issue - error in documentation needs-triage Waiting - Needs triage labels Dec 31, 2024
@surfingoldelephant surfingoldelephant changed the title Add missing information to about_Member-Access_Enumeration on termination behavior and $null output Misleading or missing information on member-access enumeration Jan 1, 2025
@sdwheeler sdwheeler added area-about Area - About_ topics and removed needs-triage Waiting - Needs triage labels Jan 2, 2025
@sdwheeler sdwheeler self-assigned this Jan 3, 2025
sdwheeler added a commit that referenced this issue Jan 3, 2025
…or cases (#11627)

* Clarify member-access enumeration behavior and add error cases

* fix typo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-about Area - About_ topics issue-doc-bug Issue - error in documentation
Projects
None yet
2 participants