diff --git a/src/System.Management.Automation/engine/Attributes.cs b/src/System.Management.Automation/engine/Attributes.cs index dac3a2ac377..56a5617d459 100644 --- a/src/System.Management.Automation/engine/Attributes.cs +++ b/src/System.Management.Automation/engine/Attributes.cs @@ -805,10 +805,10 @@ 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.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/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 335a704dd62..0dca2ac67b9 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); } @@ -7844,11 +7847,14 @@ 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(); - foreach (var typeAst in typeAsts.Where(ast => pattern.IsMatch(ast.Name))) + 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; if (typeAst.IsInterface) 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 0325cf94aeb..9ec50d2482d 100644 --- a/src/System.Management.Automation/engine/parser/ast.cs +++ b/src/System.Management.Automation/engine/parser/ast.cs @@ -2766,6 +2766,35 @@ 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 => + { + 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; + } + } + 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..8327a40d645 100644 --- a/test/powershell/Language/Classes/Scripting.Classes.BasicParsing.Tests.ps1 +++ b/test/powershell/Language/Classes/Scripting.Classes.BasicParsing.Tests.ps1 @@ -735,6 +735,100 @@ 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 + } + + 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" { It "Derived class method call" {"abc".ToString() | Should -BeExactly "abc" } # call [object] ToString() method as a base class method. 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 = @( @{