From c512f0a1a33b87d8d04618d1a1b005e6dec1cd19 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 28 Feb 2022 15:37:23 -0800 Subject: [PATCH 1/4] Fix the parsing code for method generic arguments --- .../engine/parser/Parser.cs | 46 +++++++--- .../engine/parser/ast.cs | 92 +++---------------- .../Parser/MethodInvocation.Tests.ps1 | 57 +++++++++--- 3 files changed, 92 insertions(+), 103 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index a855be0b0a7..c2f29053df0 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -7760,9 +7760,10 @@ private ExpressionAst MemberAccessRule(ExpressionAst targetExpr, Token operatorT } else if (_ungotToken == null) { - // Member name may be an incomplete token like `$a.$(Command-Name`; we do not look for generic args or - // invocation token(s) if the member name token is recognisably incomplete. - genericTypeArguments = GenericMethodArgumentsRule(out Token rBracket); + // Member name may be an incomplete token like `$a.$(Command-Name`, in which case, '_ungotToken != null'. + // We do not look for generic args or invocation token if the member name token is recognisably incomplete. + int resyncIndex = _tokenizer.GetRestorePoint(); + genericTypeArguments = GenericMethodArgumentsRule(resyncIndex, out Token rBracket); Token lParen = NextInvokeMemberToken(); if (lParen != null) @@ -7778,6 +7779,13 @@ private ExpressionAst MemberAccessRule(ExpressionAst targetExpr, Token operatorT "member and paren must be adjacent when the method is not generic"); return MemberInvokeRule(targetExpr, lParen, operatorToken, member, genericTypeArguments); } + else if (rBracket != null) + { + // We had a legit section of generic arguments but no 'lParen' following that, so this is not a method + // invocation, but an invalid indexing operation. Resync the tokenizer back to before the generic arg + // parsing and then continue. + Resync(resyncIndex); + } } return new MemberExpressionAst( @@ -7785,15 +7793,13 @@ private ExpressionAst MemberAccessRule(ExpressionAst targetExpr, Token operatorT targetExpr, member, @static: operatorToken.Kind == TokenKind.ColonColon, - nullConditional: operatorToken.Kind == TokenKind.QuestionDot, - genericTypeArguments); + nullConditional: operatorToken.Kind == TokenKind.QuestionDot); } - private List GenericMethodArgumentsRule(out Token rBracketToken) + private List GenericMethodArgumentsRule(int resyncIndex, out Token rBracketToken) { List genericTypes = null; - int resyncIndex = _tokenizer.GetRestorePoint(); Token lBracket = NextToken(); rBracketToken = null; @@ -7817,7 +7823,23 @@ private List GenericMethodArgumentsRule(out Token rBracketToken) SkipNewlines(); Token firstToken = NextToken(); - if (firstToken.Kind == TokenKind.Identifier || firstToken.Kind == TokenKind.LBracket) + + // For method generic arguments, we only support the syntax `$var.Method[TypeName1 <, TypeName2 ...>]`, + // not the syntax `$var.Method[[TypeName1] <, [TypeName2] ...>]`. + // The latter syntax has been supported for type expression since the beginning, but it's ambiguous in + // this scenario because we could be looking at an indexing operation on a property like: + // `$var.Property[]` + // and the '' could start with a type expression like `[TypeName]::Method()`, or even just + // a single type expression acting as a key to a hashtable property. Such cases will cause ambiguities. + // + // It could be possible to write code that sorts out the ambiguity and continue to support the latter + // syntax for method generic arguments, and thus to allow assembly-qualified type names. But we choose + // to not do so becuase: + // 1. that will definitely increase the complexity of the parsing code and also make it fragile; + // 2. the latter syntax hurts readability due to the number of opening/closing brackets. + // The downside is that the assembly-qualified type names won't be supported for method generic args, + // but that's likely not a problem in practice, and we can revisit if it turns out otherwise. + if (firstToken.Kind == TokenKind.Identifier) { resyncIndex = -1; genericTypes = GenericTypeArgumentsRule(firstToken, out rBracketToken); @@ -7836,11 +7858,11 @@ private List GenericMethodArgumentsRule(out Token rBracketToken) finally { SetTokenizerMode(oldTokenizerMode); - } - if (resyncIndex > 0) - { - Resync(resyncIndex); + if (resyncIndex > 0) + { + Resync(resyncIndex); + } } return genericTypes; diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs index 8671f3c58f2..bb817ec3d78 100644 --- a/src/System.Management.Automation/engine/parser/ast.cs +++ b/src/System.Management.Automation/engine/parser/ast.cs @@ -8012,16 +8012,10 @@ public class MemberExpressionAst : ExpressionAst, ISupportsAssignment /// True if the '::' operator was used, false if '.' is used. /// True if the member access is for a static member, using '::', false if accessing a member on an instance using '.'. /// - /// The generic type arguments passed to the member. /// /// If , , or is null. /// - public MemberExpressionAst( - IScriptExtent extent, - ExpressionAst expression, - CommandElementAst member, - bool @static, - IList genericTypes) + public MemberExpressionAst(IScriptExtent extent, ExpressionAst expression, CommandElementAst member, bool @static) : base(extent) { if (expression == null || member == null) @@ -8034,35 +8028,6 @@ public MemberExpressionAst( this.Member = member; SetParent(member); this.Static = @static; - - if (genericTypes is not null && genericTypes.Count > 0) - { - this.GenericTypeArguments = new ReadOnlyCollection(genericTypes); - } - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The extent of the expression, starting with the expression before the operator '.' or '::' and ending after - /// membername or expression naming the member. - /// - /// The expression before the member access operator '.' or '::'. - /// The name or expression naming the member to access. - /// True if the '::' operator was used, false if '.' is used. - /// True if the member access is for a static member, using '::', false if accessing a member on an instance using '.'. - /// - /// - /// If , , or is null. - /// - public MemberExpressionAst( - IScriptExtent extent, - ExpressionAst expression, - CommandElementAst member, - bool @static) - : this(extent, expression, member, @static, genericTypes: null) - { } /// @@ -8076,46 +8041,15 @@ public MemberExpressionAst( /// The name or expression naming the member to access. /// True if the '::' operator was used, false if '.' or '?.' is used. /// True if '?.' used. - /// The generic type arguments passed to the member. /// /// If , , or is null. /// - public MemberExpressionAst( - IScriptExtent extent, - ExpressionAst expression, - CommandElementAst member, - bool @static, - bool nullConditional, - IList genericTypes) - : this(extent, expression, member, @static, genericTypes) + public MemberExpressionAst(IScriptExtent extent, ExpressionAst expression, CommandElementAst member, bool @static, bool nullConditional) + : this(extent, expression, member, @static) { this.NullConditional = nullConditional; } - /// - /// Initializes a new instance of the class. - /// - /// - /// The extent of the expression, starting with the expression before the operator '.', '::' or '?.' and ending after - /// membername or expression naming the member. - /// - /// The expression before the member access operator '.', '::' or '?.'. - /// The name or expression naming the member to access. - /// True if the '::' operator was used, false if '.' or '?.' is used. - /// True if '?.' used. - /// - /// If , , or is null. - /// - public MemberExpressionAst( - IScriptExtent extent, - ExpressionAst expression, - CommandElementAst member, - bool @static, - bool nullConditional) - : this(extent, expression, member, @static, nullConditional, genericTypes: null) - { - } - /// /// The expression that produces the value to retrieve the member from. This property is never null. /// @@ -8136,11 +8070,6 @@ public MemberExpressionAst( /// public bool NullConditional { get; protected set; } - /// - /// Gets a list of generic type arguments passed to this member. - /// - public ReadOnlyCollection GenericTypeArguments { get; } - /// /// Copy the MemberExpressionAst instance. /// @@ -8154,8 +8083,7 @@ public override Ast Copy() newExpression, newMember, this.Static, - this.NullConditional, - this.GenericTypeArguments); + this.NullConditional); } #region Visitors @@ -8214,13 +8142,18 @@ public InvokeMemberExpressionAst( IEnumerable arguments, bool @static, IList genericTypes) - : base(extent, expression, method, @static, genericTypes) + : base(extent, expression, method, @static) { if (arguments != null && arguments.Any()) { this.Arguments = new ReadOnlyCollection(arguments.ToArray()); SetParents(Arguments); } + + if (genericTypes != null && genericTypes.Count > 0) + { + this.GenericTypeArguments = new ReadOnlyCollection(genericTypes); + } } /// @@ -8308,6 +8241,11 @@ public InvokeMemberExpressionAst( { } + /// + /// Gets a list of generic type arguments passed to this method invocation. + /// + public ReadOnlyCollection GenericTypeArguments { get; } + /// /// The non-empty collection of arguments to pass when invoking the method, or null if no arguments were specified. /// diff --git a/test/powershell/Language/Parser/MethodInvocation.Tests.ps1 b/test/powershell/Language/Parser/MethodInvocation.Tests.ps1 index e60f8a2b148..ff7e49105a2 100644 --- a/test/powershell/Language/Parser/MethodInvocation.Tests.ps1 +++ b/test/powershell/Language/Parser/MethodInvocation.Tests.ps1 @@ -13,9 +13,18 @@ Describe 'Generic Method invocation' -Tags 'CI' { Script = '[Array]::Empty[System.Collections.Generic.Dictionary[System.Numerics.BigInteger, System.Collections.Generic.List[string[,]]]]()' ExpectedType = [System.Collections.Generic.Dictionary[System.Numerics.BigInteger, System.Collections.Generic.List[string[, ]]][]] } + ) + + $IndexingAProperty = @( + @{ + Script = '[object]::Property[[type]]' + IndexType = 'System.Management.Automation.Language.TypeExpressionAst' + IndexString = '[type]' + } @{ - Script = '[Array]::$("Empty")[[System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Numerics.BigInteger, System.Runtime.Numerics]], System.Private.CoreLib]]()' - ExpectedType = [System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib], [System.Numerics.BigInteger, System.Runtime.Numerics]][], System.Private.CoreLib] + Script = '$object.IPSubnet[[Array]::IndexOf($_.IPAddress, $_.IPAddress[0])]' + IndexType = 'System.Management.Automation.Language.InvokeMemberExpressionAst' + IndexString = '[Array]::IndexOf($_.IPAddress, $_.IPAddress[0])' } ) @@ -37,8 +46,8 @@ Describe 'Generic Method invocation' -Tags 'CI' { } @{ Script = '[array]::empty[type]]()' - ExpectedErrors = @('UnexpectedToken', 'ExpectedExpression') - ErrorCount = 2 + ExpectedErrors = @('MissingArrayIndexExpression', 'UnexpectedToken', 'ExpectedExpression') + ErrorCount = 3 } @{ Script = '$object.Method[type,]()' @@ -70,6 +79,21 @@ Describe 'Generic Method invocation' -Tags 'CI' { ExpectedErrors = @('EndSquareBracketExpectedAtEndOfType', 'UnexpectedToken') ErrorCount = 2 } + @{ + Script = '$object.Method[[type]]()' + ExpectedErrors = @('UnexpectedToken', 'ExpectedExpression') + ErrorCount = 2 + } + @{ + Script = '[Array]::Empty[[type]]()' + ExpectedErrors = @('UnexpectedToken', 'ExpectedExpression') + ErrorCount = 2 + } + @{ + Script = '$object.Property[type]' + ExpectedErrors = @('MissingArrayIndexExpression', 'UnexpectedToken') + ErrorCount = 2 + } ) } @@ -79,6 +103,21 @@ Describe 'Generic Method invocation' -Tags 'CI' { { [scriptblock]::Create($script) } | Should -Not -Throw } + It "parses fine for indexing a property" -TestCases $IndexingAProperty { + param($Script, $IndexType, $IndexString) + + $parseErrors = $null + + $ast = [System.Management.Automation.Language.Parser]::ParseInput($Script, [ref]$null, [ref]$parseErrors) + $parseErrors | Should -BeNullOrEmpty + + $cmdExpr = $ast.EndBlock.Statements[0].PipelineElements[0] + $cmdExpr | Should -BeOfType 'System.Management.Automation.Language.CommandExpressionAst' + $cmdExpr.Expression | Should -BeOfType 'System.Management.Automation.Language.IndexExpressionAst' + $cmdExpr.Expression.Index | Should -BeOfType $IndexType + $cmdExpr.Expression.Index.ToString() | Should -BeExactly $IndexString + } + It 'reports a parse error for "