Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/System.Management.Automation/engine/parser/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1487,7 +1487,7 @@ private ITypeName GenericTypeNameRule(Token genericTypeName, Token firstToken, b
rBracketToken = null;
}

var openGenericType = new TypeName(genericTypeName.Extent, genericTypeName.Text);
var openGenericType = new TypeName(genericTypeName.Extent, genericTypeName.Text, genericArguments.Count);
var result = new GenericTypeName(
ExtentOf(genericTypeName.Extent, ExtentFromFirstOf(rBracketToken, genericArguments.LastOrDefault(), firstToken)),
openGenericType,
Expand Down
72 changes: 61 additions & 11 deletions src/System.Management.Automation/engine/parser/ast.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7263,7 +7263,7 @@ internal PipelineAst GenerateCommandCallPipelineAst()
FunctionName.Extent,
new TypeName(
FunctionName.Extent,
typeof(System.Management.Automation.Language.DynamicKeyword).FullName)),
typeof(DynamicKeyword).FullName)),
new StringConstantExpressionAst(
FunctionName.Extent,
"GetKeyword",
Expand Down Expand Up @@ -8422,9 +8422,11 @@ internal interface ISupportsTypeCaching
/// </summary>
public sealed class TypeName : ITypeName, ISupportsTypeCaching
{
internal readonly string _name;
internal Type _type;
internal readonly IScriptExtent _extent;
private readonly string _name;
private readonly IScriptExtent _extent;
private readonly int _genericArgumentCount;
private Type _type;

internal TypeDefinitionAst _typeDefinitionAst;

/// <summary>
Expand Down Expand Up @@ -8483,6 +8485,23 @@ public TypeName(IScriptExtent extent, string name, string assembly)
AssemblyName = assembly;
}

/// <summary>
/// Construct a typename that represents a generic type definition.
/// </summary>
/// <param name="extent">The extent of the typename.</param>
/// <param name="name">The name of the type.</param>
/// <param name="genericArgumentCount">The number of generic arguments.</param>
internal TypeName(IScriptExtent extent, string name, int genericArgumentCount)
: this(extent, name)
{
ArgumentOutOfRangeException.ThrowIfLessThan(genericArgumentCount, 0);

if (genericArgumentCount > 0 && !_name.Contains('`'))
{
_genericArgumentCount = genericArgumentCount;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Usually, we expect a constructor to throw an exception if the argument is unexpected. But here we perform a correction of the argument. It's amazing. I'm afraid this may provoke accidental misuse of this constructor in the future. At the very least, it would be good to add a comment with examples of the scenarios we address and explaining why this correction is needed in this place.
Alternatively, we could perform a check at the call location and pass the correct arguments.
It could be even better to use a template like private static TypeName TypeName.CreateAlternativeGenericTypeName() with a comprehensive description.

Copy link
Member Author

Choose a reason for hiding this comment

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

This "try alternate name" thing happens in multiple places, which is common when dealing with generic type resolution. Comments were added in the GetReflectionType() to explain why we only fall back to alternate name even if genericArgumentCount is set.

As for the scenario for using this constructor, the XML comment calls it out and it's also easy to understand by looking at where it's used. So, I don't think more comments are needed.

I can add a check to throw when genericArgumentCount is a negative value.

}

/// <summary>
/// Returns the full name of the type.
/// </summary>
Expand Down Expand Up @@ -8559,8 +8578,25 @@ public Type GetReflectionType()
{
if (_type == null)
{
Exception e;
Type type = _typeDefinitionAst != null ? _typeDefinitionAst.Type : TypeResolver.ResolveTypeName(this, out e);
Type type = _typeDefinitionAst != null ? _typeDefinitionAst.Type : TypeResolver.ResolveTypeName(this, out _);

if (type is null && _genericArgumentCount > 0)
{
// We try an alternate name only if it failed to resolve with the original name.
// This is because for a generic type like `System.Tuple<type1, type2>`, the original name `System.Tuple`
// can be resolved and hence `genericTypeName.TypeName.GetReflectionType()` in that case has always been
// returning the type `System.Tuple`. If we change to directly use the alternate name for resolution, the
// return value will become 'System.Tuple`1' in that case, and that's a breaking change.
TypeName newTypeName = new(
_extent,
string.Create(CultureInfo.InvariantCulture, $"{_name}`{_genericArgumentCount}"))
{
AssemblyName = AssemblyName
};

type = TypeResolver.ResolveTypeName(newTypeName, out _);
}

if (type != null)
{
try
Expand Down Expand Up @@ -8595,7 +8631,11 @@ public Type GetReflectionAttributeType()
var result = GetReflectionType();
if (result == null || !typeof(Attribute).IsAssignableFrom(result))
{
var attrTypeName = new TypeName(_extent, FullName + "Attribute");
TypeName attrTypeName = new(_extent, $"{_name}Attribute", _genericArgumentCount)
{
AssemblyName = AssemblyName
};

result = attrTypeName.GetReflectionType();
if (result != null && !typeof(Attribute).IsAssignableFrom(result))
{
Expand Down Expand Up @@ -8888,8 +8928,13 @@ internal Type GetGenericType(Type generic)
{
if (!TypeName.FullName.Contains('`'))
{
var newTypeName = new TypeName(Extent,
string.Create(CultureInfo.InvariantCulture, $"{TypeName.FullName}`{GenericArguments.Count}"));
TypeName newTypeName = new(
Extent,
string.Create(CultureInfo.InvariantCulture, $"{TypeName.Name}`{GenericArguments.Count}"))
{
AssemblyName = TypeName.AssemblyName
};

generic = newTypeName.GetReflectionType();
}
}
Expand All @@ -8915,8 +8960,13 @@ public Type GetReflectionAttributeType()
{
if (!TypeName.FullName.Contains('`'))
{
var newTypeName = new TypeName(Extent,
string.Create(CultureInfo.InvariantCulture, $"{TypeName.FullName}Attribute`{GenericArguments.Count}"));
TypeName newTypeName = new(
Extent,
string.Create(CultureInfo.InvariantCulture, $"{TypeName.Name}Attribute`{GenericArguments.Count}"))
{
AssemblyName = TypeName.AssemblyName
};

generic = newTypeName.GetReflectionType();
}
}
Expand Down
47 changes: 47 additions & 0 deletions test/powershell/Language/Parser/Parsing.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,53 @@ Describe "Additional tests" -Tag CI {
$result.EndBlock.Statements[0].PipelineElements[0].Expression.TypeName.FullName | Should -Be 'System.Tuple[System.String[],System.Int32[]]'
}

It "Should correctly set the cached type for 'GenericTypeName.TypeName' as needed when the generic type is found in cache" {
$tks = $null
$ers = $null
$Script = '[System.Collections.Generic.List[string]]'

## See https://github.com/PowerShell/PowerShell/issues/24982 for details about the issue.
$result = [System.Management.Automation.Language.Parser]::ParseInput($Script, [ref]$tks, [ref]$ers)
$typeExpr = $result.EndBlock.Statements[0].PipelineElements[0].Expression
$typeExpr.TypeName.FullName | Should -Be 'System.Collections.Generic.List[string]'
$typeExpr.TypeName.TypeName.FullName | Should -Be 'System.Collections.Generic.List'
$typeExpr.TypeName.TypeName.GetReflectionType() | Should -Not -BeNullOrEmpty
$typeExpr.TypeName.TypeName.GetReflectionType().FullName | Should -Be 'System.Collections.Generic.List`1'

$result2 = [System.Management.Automation.Language.Parser]::ParseInput($Script, [ref]$tks, [ref]$ers)
$typeExpr2 = $result2.EndBlock.Statements[0].PipelineElements[0].Expression
$typeExpr2.TypeName.FullName | Should -Be 'System.Collections.Generic.List[string]'
$typeExpr2.TypeName.TypeName.FullName | Should -Be 'System.Collections.Generic.List'
$typeExpr2.TypeName.TypeName.GetReflectionType() | Should -Not -BeNullOrEmpty
$typeExpr2.TypeName.TypeName.GetReflectionType().FullName | Should -Be 'System.Collections.Generic.List`1'

$Script = '[System.Tuple[System.String[],System.Int32[]]]'
$result3 = [System.Management.Automation.Language.Parser]::ParseInput($Script, [ref]$tks, [ref]$ers)
$result3.EndBlock.Statements[0].PipelineElements[0].Expression.TypeName.TypeName.GetReflectionType().FullName | Should -Be 'System.Tuple'

## Generic type with assembly name can be resolved.
[System.Collections.Generic.List[string], System.Private.CoreLib].FullName | Should -BeLike 'System.Collections.Generic.List``1`[`[System.String, *`]`]'

$Script = '[System.Collections.Generic.List[string], System.Private.CoreLib]'
$result4 = [System.Management.Automation.Language.Parser]::ParseInput($Script, [ref]$tks, [ref]$ers)
$typeExpr4 = $result4.EndBlock.Statements[0].PipelineElements[0].Expression
$typeExpr4.TypeName.FullName | Should -Be 'System.Collections.Generic.List[string],System.Private.CoreLib'
$typeExpr4.TypeName.TypeName.FullName | Should -Be 'System.Collections.Generic.List,System.Private.CoreLib'
$typeExpr4.TypeName.TypeName.GetReflectionType() | Should -Not -BeNullOrEmpty
$typeExpr4.TypeName.TypeName.GetReflectionType().FullName | Should -Be 'System.Collections.Generic.List`1'

## Generic type with '`<n>' in name can be resolved.
[System.Collections.Generic.List`1[string]].FullName | Should -BeLike 'System.Collections.Generic.List``1`[`[System.String, *`]`]'

$Script = '[System.Collections.Generic.List`1[string]]'
$result5 = [System.Management.Automation.Language.Parser]::ParseInput($Script, [ref]$tks, [ref]$ers)
$typeExpr5 = $result5.EndBlock.Statements[0].PipelineElements[0].Expression
$typeExpr5.TypeName.FullName | Should -Be 'System.Collections.Generic.List`1[string]'
$typeExpr5.TypeName.TypeName.FullName | Should -Be 'System.Collections.Generic.List`1'
$typeExpr5.TypeName.TypeName.GetReflectionType() | Should -Not -BeNullOrEmpty
$typeExpr5.TypeName.TypeName.GetReflectionType().FullName | Should -Be 'System.Collections.Generic.List`1'
}

It "Should get correct offsets for number constant parsing error" {
$tks = $null
$ers = $null
Expand Down
Loading