diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/OutGridView/OutGridViewCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/OutGridView/OutGridViewCommand.cs index 574ca39426d..945b713f7e6 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/OutGridView/OutGridViewCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/OutGridView/OutGridViewCommand.cs @@ -312,7 +312,7 @@ internal GridHeader(OutGridViewCommand parentCmd) internal static GridHeader ConstructGridHeader(PSObject input, OutGridViewCommand parentCmd) { if (DefaultScalarTypes.IsTypeInList(input.TypeNames) || - !OutOfBandFormatViewManager.HasNonRemotingProperties(input)) + OutOfBandFormatViewManager.IsPropertyLessObject(input)) { return new ScalarTypeHeader(parentCmd, input); } diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewManager.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewManager.cs index 891dfce6829..8611ef5759e 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewManager.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewManager.cs @@ -465,19 +465,46 @@ private static ViewGenerator SelectViewGeneratorFromProperties(FormatShape shape /// internal static class OutOfBandFormatViewManager { + internal static bool IsPropertyLessObject(PSObject so) + { + // Using an enumerator to avoid the ExpandAll costs + var propertiesEnumerator = so.Properties.GetEnumerator(); + + try + { + // Manually enumerate properties - Maximum iterations : 6 (5 Remoting properties + 1) + while (propertiesEnumerator.MoveNext()) + { + var property = propertiesEnumerator.Current; + + // Skip remoting properties + if (IsNotRemotingProperty(property.Name)) + { + // Found a non-remoting property - object is NOT propertyless + return false; + } + } + + // Only remoting properties found (or no properties at all) + return true; + } + finally + { + // Clean up the enumerator if it's IDisposable + (propertiesEnumerator as IDisposable)?.Dispose(); + } + } + private static bool IsNotRemotingProperty(string name) { var isRemotingPropertyName = name.Equals(RemotingConstants.ComputerNameNoteProperty, StringComparison.OrdinalIgnoreCase) || name.Equals(RemotingConstants.ShowComputerNameNoteProperty, StringComparison.OrdinalIgnoreCase) || name.Equals(RemotingConstants.RunspaceIdNoteProperty, StringComparison.OrdinalIgnoreCase) - || name.Equals(RemotingConstants.SourceJobInstanceId, StringComparison.OrdinalIgnoreCase); + || name.Equals(RemotingConstants.SourceJobInstanceId, StringComparison.OrdinalIgnoreCase) + || name.Equals(RemotingConstants.EventObject, StringComparison.OrdinalIgnoreCase); return !isRemotingPropertyName; } - private static readonly MemberNamePredicate NameIsNotRemotingProperty = IsNotRemotingProperty; - - internal static bool HasNonRemotingProperties(PSObject so) => so.GetFirstPropertyOrDefault(NameIsNotRemotingProperty) != null; - internal static FormatEntryData GenerateOutOfBandData(TerminatingErrorContext errorContext, PSPropertyExpressionFactory expressionFactory, TypeInfoDataBase db, PSObject so, int enumerationLimit, bool useToStringFallback, out List errors) { @@ -503,8 +530,9 @@ internal static FormatEntryData GenerateOutOfBandData(TerminatingErrorContext er } else { + // If we have a scalar type, or no visible properties, we fallback to a ToString call if (DefaultScalarTypes.IsTypeInList(typeNames) - || !HasNonRemotingProperties(so)) + || IsPropertyLessObject(so)) { // we force a ToString() on well known types return GenerateOutOfBandObjectAsToString(so); @@ -515,12 +543,6 @@ internal static FormatEntryData GenerateOutOfBandData(TerminatingErrorContext er return null; } - // we must check we have enough properties for a list view - if (new PSPropertyExpression("*").ResolveNames(so).Count == 0) - { - return null; - } - // we do not have a view, we default to list view // process an out of band view as a default outOfBandViewGenerator = new ListViewGenerator(); diff --git a/test/powershell/engine/Formatting/BugFix.Tests.ps1 b/test/powershell/engine/Formatting/BugFix.Tests.ps1 index 5be7797134c..bb114b3981b 100644 --- a/test/powershell/engine/Formatting/BugFix.Tests.ps1 +++ b/test/powershell/engine/Formatting/BugFix.Tests.ps1 @@ -46,6 +46,27 @@ Describe "Hidden properties should not be returned by the 'FirstOrDefault' primi $outstring.Trim() | Should -BeLike "*.Hidden2" } + It "Formatting for an object with only hidden property should use 'ToString' after a Get-Member call" { + class Hidden { + hidden $Param = 'Foo' + [String]ToString() { return 'MyString' } + } + + $hiddenObjectOne = [Hidden]::new() + $hiddenObjectOne | Get-Member | Out-Null + $outstring = $hiddenObjectOne | Out-String + $outstring.Trim() | Should -BeExactly "MyString" + + class Hidden2 { + hidden $Param = 'Foo' + } + + $hiddenObjectTwo = [Hidden2]::new() + $hiddenObjectTwo | Get-Member | Out-Null + $outstring = $hiddenObjectTwo | Out-String + $outstring.Trim() | Should -BeLike "*.Hidden2" + } + It 'Formatting for an object with no-hidden property should use the default view' { class Params { $Param = 'Foo'