From 7bfa62ffde9214f1707369245eb41f53e687c770 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Sun, 23 Nov 2025 22:50:45 +0900 Subject: [PATCH 1/4] Allow HiddenAttribute to decorate classes and exclude from type completion - Extend AttributeUsage to include AttributeTargets.Class - Add IsHidden property to TypeDefinitionAst with caching - Filter hidden classes from type name completion in CompleteType - Add comprehensive tests for hidden class functionality --- .../engine/Attributes.cs | 2 +- .../CommandCompletion/CompletionCompleters.cs | 5 ++- .../engine/parser/ast.cs | 15 +++++++ .../Scripting.Classes.BasicParsing.Tests.ps1 | 39 +++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/Attributes.cs b/src/System.Management.Automation/engine/Attributes.cs index dac3a2ac377..998731ecf25 100644 --- a/src/System.Management.Automation/engine/Attributes.cs +++ b/src/System.Management.Automation/engine/Attributes.cs @@ -808,7 +808,7 @@ public sealed class PSDefaultValueAttribute : ParsingBaseAttribute /// Specify that the member is hidden for the purposes of cmdlets like Get-Member and that the /// member is not displayed by default by Format-* cmdlets. /// - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Event)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Event)] public sealed class HiddenAttribute : ParsingBaseAttribute { } diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 335a704dd62..30e287f06e1 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -7644,6 +7644,9 @@ private static TypeCompletionMapping[][] InitializeTypeCache() // Ignore non-public types if (!TypeResolver.IsPublic(type)) { continue; } + // Ignore types with Hidden attribute + if (type.IsDefined(typeof(HiddenAttribute), false)) { continue; } + HandleNamespace(entries, type.Namespace); HandleType(entries, type.FullName, type.Name, type); } @@ -7848,7 +7851,7 @@ internal static List CompleteType(CompletionContext context, s { var scriptBlockAst = (ScriptBlockAst)context.RelatedAsts[0]; var typeAsts = scriptBlockAst.FindAll(static ast => ast is TypeDefinitionAst, false).Cast(); - foreach (var typeAst in typeAsts.Where(ast => pattern.IsMatch(ast.Name))) + foreach (var typeAst in typeAsts.Where(ast => pattern.IsMatch(ast.Name) && !ast.IsHidden)) { string toolTipPrefix = string.Empty; if (typeAst.IsInterface) diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs index 0325cf94aeb..4e95bd6e995 100644 --- a/src/System.Management.Automation/engine/parser/ast.cs +++ b/src/System.Management.Automation/engine/parser/ast.cs @@ -2766,6 +2766,21 @@ public TypeDefinitionAst(IScriptExtent extent, string name, IEnumerable public bool IsInterface { get { return (TypeAttributes & TypeAttributes.Interface) == TypeAttributes.Interface; } } + private bool? _isHidden; + + /// + /// Returns true if the type has the Hidden attribute. + /// + public bool IsHidden + { + get + { + _isHidden ??= Attributes.Any(attr => attr.TypeName.GetReflectionAttributeType() == typeof(HiddenAttribute)); + + return _isHidden.Value; + } + } + internal Type Type { get diff --git a/test/powershell/Language/Classes/Scripting.Classes.BasicParsing.Tests.ps1 b/test/powershell/Language/Classes/Scripting.Classes.BasicParsing.Tests.ps1 index 6674697ca2f..bfbc43f8cf0 100644 --- a/test/powershell/Language/Classes/Scripting.Classes.BasicParsing.Tests.ps1 +++ b/test/powershell/Language/Classes/Scripting.Classes.BasicParsing.Tests.ps1 @@ -735,6 +735,45 @@ visibleX visibleY It "Tab completion should not return a hidden member" { $completions.CompletionMatches.Count | Should -Be 0 } } +Describe 'HiddenAttribute on Class Test' -Tags "CI" { + BeforeAll { + # Define a class with HiddenAttribute using C# + $hiddenClassSource = @" +using System.Management.Automation; + +[Hidden] +public class HiddenTestClass +{ + public string Name { get; set; } + public int Value { get; set; } +} +"@ + Add-Type -TypeDefinition $hiddenClassSource + } + + It "Should be able to create an instance of hidden class" { + $instance = [HiddenTestClass]::new() + $instance.Name = "Test" + $instance.Value = 42 + $instance.Name | Should -Be "Test" + $instance.Value | Should -Be 42 + } + + It "HiddenAttribute should be present on the class" { + $hiddenAttr = [HiddenTestClass].GetCustomAttributes([System.Management.Automation.HiddenAttribute], $false) + $hiddenAttr.Count | Should -BeGreaterThan 0 + } + + It "Hidden class should not appear in type name completion" { + # Get tab completion results for type names starting with "HiddenTest" + $result = TabExpansion2 -inputScript '[HiddenTest' -cursorColumn '[HiddenTest'.Length + $completions = $result.CompletionMatches | Where-Object { $_.CompletionText -eq 'HiddenTestClass' } + + # HiddenTestClass should not be in completion results + $completions | Should -BeNullOrEmpty + } +} + Describe 'BaseMethodCall Test ' -Tags "CI" { It "Derived class method call" {"abc".ToString() | Should -BeExactly "abc" } # call [object] ToString() method as a base class method. From 6c3b979d26192ef68a3cea37e57394dedf36ed30 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Mon, 24 Nov 2025 16:01:26 +0900 Subject: [PATCH 2/4] Update src/System.Management.Automation/engine/Attributes.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/System.Management.Automation/engine/Attributes.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/Attributes.cs b/src/System.Management.Automation/engine/Attributes.cs index 998731ecf25..56a5617d459 100644 --- a/src/System.Management.Automation/engine/Attributes.cs +++ b/src/System.Management.Automation/engine/Attributes.cs @@ -805,8 +805,8 @@ public sealed class PSDefaultValueAttribute : ParsingBaseAttribute } /// - /// Specify that the member is hidden for the purposes of cmdlets like Get-Member and that the - /// member is not displayed by default by Format-* cmdlets. + /// Specify that the type or member is hidden from type/member completion and that the + /// member is not displayed by default by Format-* cmdlets or Get-Member. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Event)] public sealed class HiddenAttribute : ParsingBaseAttribute From a5344fed43a5fcd14a44d1d6df86df6019e95ce0 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Tue, 25 Nov 2025 23:18:33 +0900 Subject: [PATCH 3/4] Add Hidden type accelerator and tests for PowerShell classes (Fixes #18914) --- .../CommandCompletion/CompletionAnalysis.cs | 3 + .../CommandCompletion/CompletionCompleters.cs | 9 ++- .../engine/parser/TypeResolver.cs | 1 + .../engine/parser/ast.cs | 16 +++++- .../Scripting.Classes.BasicParsing.Tests.ps1 | 55 +++++++++++++++++++ 5 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 8aafa939d84..6c2e49f6920 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -18,6 +18,8 @@ internal class CompletionContext { internal List RelatedAsts { get; set; } + internal Ast InputAst { get; set; } + // Only one of TokenAtCursor or TokenBeforeCursor is set // This is how we can tell if we're trying to complete part of something (like a member) // or complete an argument, where TokenBeforeCursor could be a parameter name. @@ -199,6 +201,7 @@ private CompletionContext InitializeCompletionContext(TypeInferenceContext typeI TokenAtCursor = astContext.TokenAtCursor, TokenBeforeCursor = astContext.TokenBeforeCursor, RelatedAsts = astContext.RelatedAsts, + InputAst = _ast, ReplacementIndex = astContext.ReplacementIndex, ExecutionContext = executionContext, TypeInferenceContext = typeInferenceContext, diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 30e287f06e1..0dca2ac67b9 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -7847,10 +7847,13 @@ internal static List CompleteType(CompletionContext context, s } // this is a temporary fix. Only the type defined in the same script get complete. Need to use using Module when that is available. - if (context.RelatedAsts != null && context.RelatedAsts.Count > 0) + // Search from InputAst (entire input buffer) first to handle inline class definitions like: [hidden()] class Test1{} [Test1 + // If InputAst is not available, fall back to RelatedAsts (cursor-adjacent ASTs only) + Ast astToSearch = context.InputAst ?? (context.RelatedAsts != null && context.RelatedAsts.Count > 0 ? context.RelatedAsts[0] : null); + + if (astToSearch != null) { - var scriptBlockAst = (ScriptBlockAst)context.RelatedAsts[0]; - var typeAsts = scriptBlockAst.FindAll(static ast => ast is TypeDefinitionAst, false).Cast(); + var typeAsts = astToSearch.FindAll(static ast => ast is TypeDefinitionAst, searchNestedScriptBlocks: true).Cast(); foreach (var typeAst in typeAsts.Where(ast => pattern.IsMatch(ast.Name) && !ast.IsHidden)) { string toolTipPrefix = string.Empty; diff --git a/src/System.Management.Automation/engine/parser/TypeResolver.cs b/src/System.Management.Automation/engine/parser/TypeResolver.cs index 72258a4f459..50f9785ebe6 100644 --- a/src/System.Management.Automation/engine/parser/TypeResolver.cs +++ b/src/System.Management.Automation/engine/parser/TypeResolver.cs @@ -744,6 +744,7 @@ internal static class CoreTypes { typeof(float), new[] { "float", "single" } }, { typeof(Guid), new[] { "guid" } }, { typeof(Hashtable), new[] { "hashtable" } }, + { typeof(HiddenAttribute), new[] { "Hidden" } }, { typeof(int), new[] { "int", "int32" } }, { typeof(Int16), new[] { "short", "int16" } }, { typeof(long), new[] { "long", "int64" } }, diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs index 4e95bd6e995..9ec50d2482d 100644 --- a/src/System.Management.Automation/engine/parser/ast.cs +++ b/src/System.Management.Automation/engine/parser/ast.cs @@ -2775,7 +2775,21 @@ public bool IsHidden { get { - _isHidden ??= Attributes.Any(attr => attr.TypeName.GetReflectionAttributeType() == typeof(HiddenAttribute)); + _isHidden ??= Attributes.Any(attr => + { + var reflectionType = attr.TypeName.GetReflectionAttributeType(); + if (reflectionType == typeof(HiddenAttribute)) + { + return true; + } + + // For inline definitions, GetReflectionAttributeType() may return null at tab completion time + // because the type hasn't been compiled yet. Check the type name as a string fallback. + var typeName = attr.TypeName.FullName; + return typeName.Equals("hidden", StringComparison.OrdinalIgnoreCase) || + typeName.Equals("HiddenAttribute", StringComparison.OrdinalIgnoreCase) || + typeName.Equals("System.Management.Automation.HiddenAttribute", StringComparison.OrdinalIgnoreCase); + }); return _isHidden.Value; } diff --git a/test/powershell/Language/Classes/Scripting.Classes.BasicParsing.Tests.ps1 b/test/powershell/Language/Classes/Scripting.Classes.BasicParsing.Tests.ps1 index bfbc43f8cf0..8327a40d645 100644 --- a/test/powershell/Language/Classes/Scripting.Classes.BasicParsing.Tests.ps1 +++ b/test/powershell/Language/Classes/Scripting.Classes.BasicParsing.Tests.ps1 @@ -772,6 +772,61 @@ public class HiddenTestClass # HiddenTestClass should not be in completion results $completions | Should -BeNullOrEmpty } + + It "Visible C# class should appear in type name completion" { + # Regression test: ensure non-hidden C# classes still appear in completion + Add-Type -TypeDefinition 'public class VisibleCSharpTestClass { public string Name; }' + $result = TabExpansion2 -inputScript '[VisibleCSharpTest' -cursorColumn '[VisibleCSharpTest'.Length + $completions = $result.CompletionMatches | Where-Object { $_.CompletionText -eq 'VisibleCSharpTestClass' } + + # VisibleCSharpTestClass should be in completion results + $completions | Should -Not -BeNullOrEmpty + } +} + +Describe 'HiddenAttribute on PowerShell Class Test' -Tags "CI" { + It "Hidden PowerShell class should not appear in inline type name completion" { + # MartinGC94's specific scenario: TabExpansion2 '[hidden()] class Test1{} [Test1' + # This tests AST-based handling of inline PowerShell class definitions + $testScript = '[hidden()] class Test1{} [Test1' + $result = TabExpansion2 -inputScript $testScript -cursorColumn $testScript.Length + $completions = $result.CompletionMatches | Where-Object { $_.CompletionText -eq 'Test1' } + + # Test1 should not be in completion results because it has [hidden()] attribute + $completions | Should -BeNullOrEmpty + } + + It "Visible PowerShell class should appear in inline type name completion" { + # Comparison test: class without [hidden()] should appear in completion + $testScript = 'class Test2{} [Test2' + $result = TabExpansion2 -inputScript $testScript -cursorColumn $testScript.Length + $completions = $result.CompletionMatches | Where-Object { $_.CompletionText -eq 'Test2' } + + # Test2 should be in completion results + $completions | Should -Not -BeNullOrEmpty + } + + It "Should be able to create an instance of hidden PowerShell class" { + # Verify that hidden classes are still functional, just not in tab completion + Invoke-Expression '[hidden()] class Test3 { [string]$Name }' + $instance = [Test3]::new() + $instance.Name = "Test" + $instance.Name | Should -Be "Test" + } + + It "Multiple visible PowerShell classes should all appear in inline type name completion" { + # Regression test: ensure multiple non-hidden classes all appear in completion + $testScript = 'class A1 {} class A2 {} class A3 {} [A' + $result = TabExpansion2 -inputScript $testScript -cursorColumn $testScript.Length + $a1 = $result.CompletionMatches | Where-Object { $_.CompletionText -eq 'A1' } + $a2 = $result.CompletionMatches | Where-Object { $_.CompletionText -eq 'A2' } + $a3 = $result.CompletionMatches | Where-Object { $_.CompletionText -eq 'A3' } + + # All three classes should be in completion results + $a1 | Should -Not -BeNullOrEmpty + $a2 | Should -Not -BeNullOrEmpty + $a3 | Should -Not -BeNullOrEmpty + } } Describe 'BaseMethodCall Test ' -Tags "CI" { From f8533fabd1ee10c689a6816f07968c3c570b816b Mon Sep 17 00:00:00 2001 From: yotsuda Date: Wed, 26 Nov 2025 09:52:33 +0900 Subject: [PATCH 4/4] Add Hidden type accelerator test case and update expected count (#18914) --- test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 b/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 index 99925600f63..027ca27902c 100644 --- a/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 +++ b/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 @@ -98,6 +98,10 @@ Describe "Type accelerators" -Tags "CI" { Accelerator = 'hashtable' Type = [System.Collections.Hashtable] } + @{ + Accelerator = 'Hidden' + Type = [System.Management.Automation.HiddenAttribute] + } @{ Accelerator = 'int' Type = [System.Int32] @@ -418,11 +422,11 @@ Describe "Type accelerators" -Tags "CI" { if ( !$IsWindows ) { - $totalAccelerators = 102 + $totalAccelerators = 103 } else { - $totalAccelerators = 107 + $totalAccelerators = 108 $extraFullPSAcceleratorTestCases = @( @{