diff --git a/src/System.Management.Automation/engine/CoreAdapter.cs b/src/System.Management.Automation/engine/CoreAdapter.cs index f78777c840b..bd5c6a53503 100644 --- a/src/System.Management.Automation/engine/CoreAdapter.cs +++ b/src/System.Management.Automation/engine/CoreAdapter.cs @@ -1983,13 +1983,39 @@ internal static void DoBoxingIfNecessary(ILGenerator generator, Type type) #endregion base } + + /// + /// The abstract cache entry type. + /// All specific cache entry types should derive from it. + /// + internal abstract class CacheEntry + { + /// + /// Gets the boolean value to indicate if the member is hidden. + /// + /// + /// Currently, we only check the 'HiddenAttribute' declared for properties and methods, + /// because it can be done for them through the 'hidden' keyword in PowerShell Class. + /// + /// We can't currently write a parameterized property in a PowerShell class so it's not too important + /// to check for the 'HiddenAttribute' for parameterized properties. But if someone added the attribute + /// to their C#, it'd be good to set this property correctly. + /// + internal virtual bool IsHidden => false; + } + /// /// Ordered and case insensitive hashtable. /// internal class CacheTable { + /// + /// An object collection is used to help make populating method cache table more efficient + /// . + /// internal Collection memberCollection; private Dictionary _indexes; + internal CacheTable() { memberCollection = new Collection(); @@ -2012,17 +2038,30 @@ internal object this[string name] return null; } - return this.memberCollection[indexObj]; + return memberCollection[indexObj]; } } + /// + /// Get the first non-hidden member that satisfies the predicate. + /// + /// + /// Hidden members are not returned for any fuzzy searches (searching by 'match' or enumerating a collection). + /// A hidden member is returned only if the member name is explicitly looked for. + /// internal object GetFirstOrDefault(MemberNamePredicate predicate) { foreach (var entry in _indexes) { if (predicate(entry.Key)) { - return this.memberCollection[entry.Value]; + object member = memberCollection[entry.Value]; + if (member is CacheEntry cacheEntry && cacheEntry.IsHidden) + { + continue; + } + + return member; } } @@ -2565,9 +2604,9 @@ private static readonly Dictionary> s_ private static readonly Dictionary> s_staticEventCacheTable = new Dictionary>(); - internal class MethodCacheEntry + internal class MethodCacheEntry : CacheEntry { - internal MethodInformation[] methodInformationStructures; + internal readonly MethodInformation[] methodInformationStructures; /// /// Cache delegate to the ctor of PSMethod<> with a template parameter derived from the methodInformationStructures. /// @@ -2585,9 +2624,33 @@ internal MethodInformation this[int i] return methodInformationStructures[i]; } } + + private bool? _isHidden; + internal override bool IsHidden + { + get + { + if (_isHidden == null) + { + bool hasHiddenAttribute = false; + foreach (var method in methodInformationStructures) + { + if (method.method.GetCustomAttributes(typeof(HiddenAttribute), inherit: false).Length != 0) + { + hasHiddenAttribute = true; + break; + } + } + + _isHidden = hasHiddenAttribute; + } + + return _isHidden.Value; + } + } } - internal class EventCacheEntry + internal class EventCacheEntry : CacheEntry { internal EventInfo[] events; @@ -2597,7 +2660,7 @@ internal EventCacheEntry(EventInfo[] events) } } - internal class ParameterizedPropertyCacheEntry + internal class ParameterizedPropertyCacheEntry : CacheEntry { internal MethodInformation[] getterInformation; internal MethodInformation[] setterInformation; @@ -2676,7 +2739,7 @@ internal ParameterizedPropertyCacheEntry(List properties) } } - internal class PropertyCacheEntry + internal class PropertyCacheEntry : CacheEntry { internal delegate object GetterDelegate(object instance); internal delegate void SetterDelegate(object instance, object setValue); @@ -2916,6 +2979,20 @@ internal SetterDelegate setterDelegate internal bool isStatic; internal Type propertyType; + private bool? _isHidden; + internal override bool IsHidden + { + get + { + if (_isHidden == null) + { + _isHidden = member.GetCustomAttributes(typeof(HiddenAttribute), inherit: false).Length != 0; + } + + return _isHidden.Value; + } + } + private AttributeCollection _attributes; internal AttributeCollection Attributes { @@ -3578,8 +3655,7 @@ private T GetDotNetPropertyImpl(object obj, string propertyName, MemberNamePr case null: return null; case PropertyCacheEntry cacheEntry when lookingForProperties: - var isHidden = cacheEntry.member.GetCustomAttributes(typeof(HiddenAttribute), false).Any(); - return new PSProperty(cacheEntry.member.Name, this, obj, cacheEntry) { IsHidden = isHidden } as T; + return new PSProperty(cacheEntry.member.Name, this, obj, cacheEntry) { IsHidden = cacheEntry.IsHidden } as T; case ParameterizedPropertyCacheEntry paramCacheEntry when lookingForParameterizedProperties: // TODO: check for HiddenAttribute @@ -3612,17 +3688,8 @@ private T GetDotNetMethodImpl(object obj, string methodName, MemberNamePredic var isCtor = methods[0].method is ConstructorInfo; bool isSpecial = !isCtor && methods[0].method.IsSpecialName; - bool isHidden = false; - foreach (var method in methods.methodInformationStructures) - { - if (method.method.GetCustomAttributes(typeof(HiddenAttribute), false).Any()) - { - isHidden = true; - break; - } - } - return PSMethod.Create(methods[0].method.Name, this, obj, methods, isSpecial, isHidden) as T; + return PSMethod.Create(methods[0].method.Name, this, obj, methods, isSpecial, methods.IsHidden) as T; } internal T GetDotNetProperty(object obj, string propertyName) where T : PSMemberInfo @@ -3712,10 +3779,12 @@ internal void AddAllProperties(object obj, PSMemberInfoInternalCollection { if (!ignoreDuplicates || (members[propertyEntry.member.Name] == null)) { - var isHidden = propertyEntry.member.GetCustomAttributes(typeof(HiddenAttribute), false).Any(); - members.Add(new PSProperty(propertyEntry.member.Name, this, - obj, propertyEntry) - { IsHidden = isHidden } as T); + members.Add( + new PSProperty( + name: propertyEntry.member.Name, + adapter: this, + baseObject: obj, + adapterData: propertyEntry) { IsHidden = propertyEntry.IsHidden } as T); } } } @@ -3754,17 +3823,7 @@ internal void AddAllMethods(object obj, PSMemberInfoInternalCollection mem if (!ignoreDuplicates || (members[name] == null)) { bool isSpecial = !isCtor && method[0].method.IsSpecialName; - bool isHidden = false; - foreach (var m in method.methodInformationStructures) - { - if (m.method.GetCustomAttributes(typeof(HiddenAttribute), false).Any()) - { - isHidden = true; - break; - } - } - - members.Add(PSMethod.Create(name, this, obj, method, isSpecial, isHidden) as T); + members.Add(PSMethod.Create(name, this, obj, method, isSpecial, method.IsHidden) as T); } } } diff --git a/src/System.Management.Automation/engine/ManagementObjectAdapter.cs b/src/System.Management.Automation/engine/ManagementObjectAdapter.cs index 1f8c2b8be98..44788eb1d4c 100644 --- a/src/System.Management.Automation/engine/ManagementObjectAdapter.cs +++ b/src/System.Management.Automation/engine/ManagementObjectAdapter.cs @@ -32,7 +32,7 @@ internal abstract class BaseWMIAdapter : Adapter /// by Get-Member cmdlet, original MethodData and computed method information such /// as whether a method is static etc. /// - internal class WMIMethodCacheEntry + internal class WMIMethodCacheEntry : CacheEntry { public string Name { get; } diff --git a/test/powershell/engine/Formatting/BugFix.Tests.ps1 b/test/powershell/engine/Formatting/BugFix.Tests.ps1 new file mode 100644 index 00000000000..65b71113389 --- /dev/null +++ b/test/powershell/engine/Formatting/BugFix.Tests.ps1 @@ -0,0 +1,53 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Describe "Hidden properties should not be returned by the 'FirstOrDefault' primitive" -Tag CI { + + It "Formatting for an object with no property/field should use 'ToString'" { + class Empty { + [String]ToString() { return 'MyString' } + } + + $outstring = [Empty]::new() | Out-String + $outstring.Trim() | Should -BeExactly "MyString" + + class Empty2 { } + + $outstring = [Empty2]::new() | Out-String + $outstring.Trim() | Should -BeLike "*.Empty2" + } + + It "Formatting for an object with only hidden property should use 'ToString'" { + class Hidden { + hidden $Param = 'Foo' + [String]ToString() { return 'MyString' } + } + + $outstring = [Hidden]::new() | Out-String + $outstring.Trim() | Should -BeExactly "MyString" + + class Hidden2 { + hidden $Param = 'Foo' + } + + $outstring = [Hidden2]::new() | 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' + [String]ToString() { return 'MyString' } + } + + $outstring = [Params]::new() | Out-String + $outstring.Trim() | Should -BeExactly "Param$([System.Environment]::NewLine)-----$([System.Environment]::NewLine)Foo" + + class Params2 { + $Param = 'Foo' + } + + $outstring = [Params2]::new() | Out-String + $outstring.Trim() | Should -BeExactly "Param$([System.Environment]::NewLine)-----$([System.Environment]::NewLine)Foo" + } +}