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"
+ }
+}