Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/System.Management.Automation/engine/CommandInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -905,7 +905,7 @@ public PSMemberNameAndType(string name, PSTypeName typeName, object value = null
}

/// <summary>
/// Represents dynamic types such as <see cref="System.Management.Automation.PSObject"/>,
/// Represents dynamic types such as <see cref="System.Management.Automation.PSCustomObject"/>,
/// but can be used where a real type might not be available, in which case the name of the type can be used.
/// The type encodes the members of dynamic objects in the type name.
/// </summary>
Expand All @@ -928,7 +928,7 @@ private PSSyntheticTypeName(string typeName, Type type, IList<PSMemberNameAndTyp
: base(typeName, type)
{
Members = membersTypes;
if (type != typeof(PSObject))
if (type != typeof(PSCustomObject))
Copy link
Collaborator

@iSazonov iSazonov Apr 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Above in lines 914 and 916 I see two Create() methods with 7 uses - all of them are subject to this change. Please describe all of them in the PR description (not only Hashtable). Perhaps they should also be reflected in the tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has already been described:

This PR updates the synthetic types used for tab completion and type inference to use PSCustomObject instead of PSObject as the default type name.

In other words, every instance where the synthetic type would say it's a "PSObject" has been updated to say it's a "PSCustomObject". This is in line with what we get at runtime, for example:

PS C:\> (ls | select -First 1 Name,FullName).GetType()

FullName                                    BaseType
--------                                    --------
System.Management.Automation.PSCustomObject System.Object

PS C:\>

As for the specific overload references you mention, the first one is exclusively used for Group-Object where the specified typename is Microsoft.PowerShell.Commands.GroupInfo so it's unaffected by these changes.
The second overload is used by Select-Object, Hashtable type inference, and convert expressions where the target type is psobject/pscustomobject. This is where I've made a slight fix so that: [psobject]@{Test = ls} is correctly treated as a hashtable, as the resulting object would just be a standard hashtable with an invisible psobject wrapper around it.

Everything should already be covered by existing tests that were modified for this change, or added as part of the change.

{
return;
}
Expand All @@ -948,7 +948,7 @@ private PSSyntheticTypeName(string typeName, Type type, IList<PSMemberNameAndTyp

private static string GetMemberTypeProjection(string typename, IList<PSMemberNameAndType> members)
{
if (typename == typeof(PSObject).FullName)
if (typename == typeof(PSCustomObject).FullName)
{
foreach (var mem in members)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ private static bool TryGetRepresentativeTypeNameFromValue(object value, out PSTy
psobjectPropertyList.Add(new PSMemberNameAndType(property.Name, propertyTypeName, property.Value));
}

type = PSSyntheticTypeName.Create(typeObject, psobjectPropertyList);
type = PSSyntheticTypeName.Create(typeof(PSCustomObject), psobjectPropertyList);
}
else
{
Expand Down Expand Up @@ -915,7 +915,10 @@ object ICustomAstVisitor.VisitConvertExpression(ConvertExpressionAst convertExpr
{
if (InferTypes(hashtableAst).FirstOrDefault() is PSSyntheticTypeName syntheticTypeName)
{
return new[] { PSSyntheticTypeName.Create(type, syntheticTypeName.Members) };
var baseSyntheticType = convertExpressionAst.Type.TypeName.FullName.EqualsOrdinalIgnoreCase(nameof(PSCustomObject))
? typeof(PSCustomObject)
: typeof(Hashtable);
return new[] { PSSyntheticTypeName.Create(baseSyntheticType, syntheticTypeName.Members) };
}
}

Expand Down Expand Up @@ -1780,7 +1783,7 @@ bool IsInPropertyArgument(object o)
foreach (var t in InferTypes(previousPipelineElementAst))
{
var list = GetMemberNameAndTypeFromProperties(t, IsInPropertyArgument);
inferredTypes.Add(PSSyntheticTypeName.Create(typeof(PSObject), list));
inferredTypes.Add(PSSyntheticTypeName.Create(typeof(PSCustomObject), list));
}
}
}
Expand Down Expand Up @@ -1944,7 +1947,7 @@ private IEnumerable<PSTypeName> InferTypesFrom(MemberExpressionAst memberExpress
var memberNameList = new List<string> { memberAsStringConst.Value };
foreach (var type in exprType)
{
if (type.Type == typeof(PSObject) && type is not PSSyntheticTypeName)
if (type.Type == typeof(PSObject))
Comment on lines -1947 to +1950
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the type is not PSSyntheticTypeName check removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added that back in: #21184 so that it could get members for synthetic types but now that synthetic types aren't PSObject anymore, that exclusion is no longer needed.

{
continue;
}
Expand Down
13 changes: 13 additions & 0 deletions test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,19 @@ namespace BugRepro
}
}

it 'should only complete selected properties' -Test {
$res = TabExpansion2 -inputScript 'Get-ChildItem | Select Name, FullName | Select '
$res.CompletionMatches.Count | Should -Be 2
$res.CompletionMatches[0].CompletionText | Should -BeExactly FullName
$res.CompletionMatches[1].CompletionText | Should -BeExactly Name
}

it 'should hashtable properties for psobject wrapped hashtable' -Test {
$res = TabExpansion2 -inputScript '([psobject]@{Test = New-Guid}).Key'
$res.CompletionMatches.Count | Should -Be 1
$res.CompletionMatches[0].CompletionText | Should -BeExactly Keys
}

It 'should complete index expression for <Intent>' -TestCases @(
@{
Intent = 'Hashtable with no user input'
Expand Down
10 changes: 5 additions & 5 deletions test/powershell/engine/Api/TypeInference.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ Describe "Type inference Tests" -tags "CI" {
}}.Ast)
$res.Count | Should -Be 1
$res[0].GetType().Name | Should -Be "PSSyntheticTypeName"
$res[0].Name | Should -Be "System.Management.Automation.PSObject#A:B"
$res[0].Name | Should -Be "System.Management.Automation.PSCustomObject#A:B"
$res[0].Members[0].Name | Should -Be "A"
$res[0].Members[0].PSTypeName | Should -Be "System.Int32"
$res[0].Members[1].Name | Should -Be "B"
Expand All @@ -544,23 +544,23 @@ Describe "Type inference Tests" -tags "CI" {
$res = [AstTypeInference]::InferTypeOf( { [io.fileinfo]::new("file") | Select-Object -Property Directory }.Ast)
$res.Count | Should -Be 1
$res[0].GetType().Name | Should -Be "PSSyntheticTypeName"
$res[0].Name | Should -Be "System.Management.Automation.PSObject#Directory"
$res[0].Name | Should -Be "System.Management.Automation.PSCustomObject#Directory"
$res[0].Members[0].Name | Should -Be "Directory"
$res[0].Members[0].PSTypeName | Should -Be "System.IO.DirectoryInfo"
}

It "Infers typeof Select-Object when PSObject and Parameter is Property" {
$res = [AstTypeInference]::InferTypeOf( { [PSCustomObject] @{A = 1; B = "2"} | Select-Object -Property A}.Ast)
$res.Count | Should -Be 1
$res[0].Name | Should -Be "System.Management.Automation.PSObject#A"
$res[0].Name | Should -Be "System.Management.Automation.PSCustomObject#A"
$res[0].Members[0].Name | Should -Be "A"
$res[0].Members[0].PSTypeName | Should -Be "System.Int32"
}

It "Infers typeof Select-Object when Parameter is Properties" {
$res = [AstTypeInference]::InferTypeOf( { [io.fileinfo]::new("file") | Select-Object -Property Director*, Name }.Ast)
$res.Count | Should -Be 1
$res[0].Name | Should -Be "System.Management.Automation.PSObject#Directory:DirectoryName:Name"
$res[0].Name | Should -Be "System.Management.Automation.PSCustomObject#Directory:DirectoryName:Name"
$res[0].Members[0].Name | Should -Be "Directory"
$res[0].Members[0].PSTypeName | Should -Be "System.IO.DirectoryInfo"
$res[0].Members[1].Name | Should -Be "DirectoryName"
Expand All @@ -570,7 +570,7 @@ Describe "Type inference Tests" -tags "CI" {
It "Infers typeof Select-Object when Parameter is ExcludeProperty" {
$res = [AstTypeInference]::InferTypeOf( { [io.fileinfo]::new("file") | Select-Object -ExcludeProperty *Time*, E* }.Ast)
$res.Count | Should -Be 1
$res[0].Name | Should -BeExactly "System.Management.Automation.PSObject#Attributes:BaseName:Directory:DirectoryName:FullName:IsReadOnly:Length:LengthString:LinkTarget:LinkType:Mode:ModeWithoutHardLink:Name:NameString:ResolvedTarget:Target:UnixFileMode:VersionInfo"
$res[0].Name | Should -BeExactly "System.Management.Automation.PSCustomObject#Attributes:BaseName:Directory:DirectoryName:FullName:IsReadOnly:Length:LengthString:LinkTarget:LinkType:Mode:ModeWithoutHardLink:Name:NameString:ResolvedTarget:Target:UnixFileMode:VersionInfo"
$names = $res[0].Members.Name
$names -contains "BaseName" | Should -BeTrue
$names -contains "Name" | Should -BeTrue
Expand Down
Loading