From 0f906d135da159fc09880da4e8c2579c101ef0e3 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Thu, 28 Mar 2024 16:57:17 +1000 Subject: [PATCH 01/29] Add parsing work for named arguments --- .../engine/parser/AstVisitor.cs | 10 ++ .../engine/parser/Compiler.cs | 8 + .../engine/parser/Parser.cs | 58 ++++++- .../engine/parser/PreOrderVisitor.cs | 3 + .../engine/parser/ast.cs | 73 +++++++++ .../engine/runtime/Binding/Binders.cs | 9 +- .../resources/ParserStrings.resx | 6 + .../Language/Parser/Parser.Tests.ps1 | 152 ++++++++++++++++++ 8 files changed, 306 insertions(+), 13 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/AstVisitor.cs b/src/System.Management.Automation/engine/parser/AstVisitor.cs index 03b234cf41f..b15d4a491e4 100644 --- a/src/System.Management.Automation/engine/parser/AstVisitor.cs +++ b/src/System.Management.Automation/engine/parser/AstVisitor.cs @@ -232,6 +232,9 @@ public interface ICustomAstVisitor2 : ICustomAstVisitor /// object? VisitPipelineChain(PipelineChainAst statementChainAst) => DefaultVisit(statementChainAst); + + /// + object? VisitLabeledExpression(LabeledExpressionAst labeledExpressionAst) => DefaultVisit(labeledExpressionAst); } #nullable restore @@ -378,6 +381,8 @@ internal AstVisitAction CheckParent(Ast ast) public override AstVisitAction VisitTernaryExpression(TernaryExpressionAst ast) => CheckParent(ast); public override AstVisitAction VisitPipelineChain(PipelineChainAst ast) => CheckParent(ast); + + public override AstVisitAction VisitLabeledExpression(LabeledExpressionAst ast) => CheckParent(ast); } /// @@ -614,6 +619,8 @@ protected AstVisitAction CheckScriptBlock(Ast ast) public override AstVisitAction VisitTernaryExpression(TernaryExpressionAst ast) { return Check(ast); } public override AstVisitAction VisitPipelineChain(PipelineChainAst ast) { return Check(ast); } + + public override AstVisitAction VisitLabeledExpression(LabeledExpressionAst ast) { return Check(ast); } } /// @@ -818,5 +825,8 @@ public abstract class DefaultCustomAstVisitor2 : DefaultCustomAstVisitor, ICusto /// public virtual object VisitPipelineChain(PipelineChainAst statementChainAst) => DefaultVisit(statementChainAst); + + /// + public virtual object VisitLabeledExpression(LabeledExpressionAst argumentWithLabelAst) => DefaultVisit(argumentWithLabelAst); } } diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs index 30528f99588..f07c03a8e61 100644 --- a/src/System.Management.Automation/engine/parser/Compiler.cs +++ b/src/System.Management.Automation/engine/parser/Compiler.cs @@ -3702,6 +3702,14 @@ public object VisitPipelineChain(PipelineChainAst pipelineChainAst) return fullyExpandedBlock; } + public object VisitLabeledExpression(LabeledExpressionAst argumentWithLabelAst) + { + string labelExpression = ((ConstantExpression)argumentWithLabelAst.Label.Visit(this)).Value.ToString() ?? string.Empty; + var valueExpression = argumentWithLabelAst.Expression.Visit(this); + // return Expression.Constant(new PSInvokeMemberBinder.ArgumentWithName(labelExpression, valueExpression)); + return valueExpression; + } + /// /// Compile a pipeline as an element in a pipeline chain. /// Needed since pure expressions won't set $? after them. diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index 464231c0402..f0878468ae5 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -7891,12 +7891,14 @@ private List InvokeParamParenListRule(Token lParen, out IScriptEx // G argument-list: '(' is passed in lParen // G argument-expression-list:opt new-lines:opt ')' // G argument-expression-list: - // G argument-expression - // G argument-expression new-lines:opt ',' argument-expression-list + // G argument-label-expression:opt argument-expression + // G argument-label-expression:opt argument-expression new-lines:opt ',' argument-expression-list // G argument-expression: // G See grammar for expression - the only difference is that an // G array-literal-expression is not allowed - the comma is used // G to separate argument-expressions. + // G argument-label-expression: + // G simple-name ':' List arguments = new List(); Token comma = null; @@ -7912,13 +7914,41 @@ private List InvokeParamParenListRule(Token lParen, out IScriptEx while (true) { SkipNewlines(); + + StringConstantExpressionAst argumentName = SimpleNameRule(out Token argNameToken); + if (argumentName is not null) + { + Token colon = NextToken(); + if (colon.Kind != TokenKind.Colon) + { + UngetToken(colon); + + ReportIncompleteInput(After(argNameToken), + nameof(ParserStrings.MissingColonAfterArgumentLabel), + ParserStrings.MissingColonAfterArgumentLabel, + argumentName.Extent.ToString(), + colon.Text); + reportedError = true; + break; + } + } + ExpressionAst argument = ExpressionRule(); if (argument == null) { - if (comma != null) - { - // ErrorRecovery: sync at closing paren or newline. + // ErrorRecovery: sync at closing paren or newline. + if (argumentName is not null) + { + ReportIncompleteInput(After(argumentName.Extent), + nameof(ParserStrings.MissingArgumentAfterLabel), + ParserStrings.MissingArgumentAfterLabel, + argumentName.Value, + comma is null ? TokenKind.RParen.Text() : TokenKind.Comma.Text()); + reportedError = true; + } + else if (comma != null) + { ReportIncompleteInput(After(comma), nameof(ParserStrings.MissingExpressionAfterToken), ParserStrings.MissingExpressionAfterToken, @@ -7929,7 +7959,14 @@ private List InvokeParamParenListRule(Token lParen, out IScriptEx break; } - arguments.Add(argument); + if (argumentName is null) + { + arguments.Add(argument); + } + else + { + arguments.Add(new LabeledExpressionAst(ExtentOf(argumentName, argument), argumentName, argument)); + } SkipNewlines(); comma = NextToken(); @@ -8127,6 +8164,15 @@ internal bool ReportIncompleteInput(IScriptExtent extent, string errorId, string return incompleteInput; } + internal bool ReportIncompleteInput(IScriptExtent extent, string errorId, string errorMsg, params object[] args) + { + // If the error position isn't at the end of the input, then we don't want to mark the error + // as incomplete input. + bool incompleteInput = _tokenizer.IsAtEndOfScript(extent, checkCommentsAndWhitespace: true); + SaveError(extent, errorId, errorMsg, incompleteInput, args); + return incompleteInput; + } + internal bool ReportIncompleteInput(IScriptExtent errorPosition, IScriptExtent errorDetectedPosition, string errorId, diff --git a/src/System.Management.Automation/engine/parser/PreOrderVisitor.cs b/src/System.Management.Automation/engine/parser/PreOrderVisitor.cs index 675e53394c6..fd66f361642 100644 --- a/src/System.Management.Automation/engine/parser/PreOrderVisitor.cs +++ b/src/System.Management.Automation/engine/parser/PreOrderVisitor.cs @@ -245,6 +245,9 @@ public abstract class AstVisitor2 : AstVisitor /// public virtual AstVisitAction VisitPipelineChain(PipelineChainAst statementChain) => DefaultVisit(statementChain); + + /// + public virtual AstVisitAction VisitLabeledExpression(LabeledExpressionAst argumentWithLabelAst) => DefaultVisit(argumentWithLabelAst); } /// diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs index c83f2189f01..270485ce048 100644 --- a/src/System.Management.Automation/engine/parser/ast.cs +++ b/src/System.Management.Automation/engine/parser/ast.cs @@ -7398,6 +7398,79 @@ internal virtual bool ShouldPreserveOutputInCaseOfException() } } +#nullable enable + + /// + /// The ast representing an expression with a string label inside a method invocation argument list, e.g. + /// $class.Method(name: $value). + /// + public class LabeledExpressionAst : ExpressionAst + { + /// + /// Initializes a new instance of the argument with label expression. + /// + /// The extent of the expression. + /// The expression label. + /// The labeled expression. + public LabeledExpressionAst( + IScriptExtent extent, + StringConstantExpressionAst label, + ExpressionAst expression) : base(extent) + { + Label = label ?? throw PSTraceSource.NewArgumentNullException(nameof(label)); + Expression = expression ?? throw PSTraceSource.NewArgumentNullException(nameof(expression)); + + SetParent(expression); + } + + /// + /// Gets the expression label. The property is never null. + /// + public StringConstantExpressionAst Label { get; } + + /// + /// Gets the labeled expression. The property is never null. + /// + public ExpressionAst Expression { get; } + + /// + /// Copy the LabeledExpressionAst instance. + /// + /// + /// Returns a copy of the ast. + /// + public override Ast Copy() + { + StringConstantExpressionAst newLabel = CopyElement(this.Label); + ExpressionAst newExpression = CopyElement(this.Expression); + return new LabeledExpressionAst(this.Extent, newLabel, newExpression); + } + + #region Visitors + + internal override object? Accept(ICustomAstVisitor visitor) + => (visitor as ICustomAstVisitor2)?.VisitLabeledExpression(this); + + internal override AstVisitAction InternalVisit(AstVisitor visitor) + { + var action = AstVisitAction.Continue; + if (visitor is AstVisitor2 visitor2) + { + action = visitor2.VisitLabeledExpression(this); + if (action == AstVisitAction.SkipChildren) + { + return visitor.CheckForPostAction(this, AstVisitAction.Continue); + } + } + + return visitor.CheckForPostAction(this, action); + } + + #endregion Visitors + } + +#nullable disable + /// /// The ast representing a ternary expression, e.g. $a ? 1 : 2. /// diff --git a/src/System.Management.Automation/engine/runtime/Binding/Binders.cs b/src/System.Management.Automation/engine/runtime/Binding/Binders.cs index de36b9d7249..751d71422ae 100644 --- a/src/System.Management.Automation/engine/runtime/Binding/Binders.cs +++ b/src/System.Management.Automation/engine/runtime/Binding/Binders.cs @@ -6542,6 +6542,8 @@ internal enum MethodInvocationType NonVirtual, } + internal sealed record ArgumentWithName(string Name, object Value); + private sealed class KeyComparer : IEqualityComparer { public bool Equals(PSInvokeMemberBinderKeyType x, PSInvokeMemberBinderKeyType y) @@ -6661,13 +6663,6 @@ public override DynamicMetaObject FallbackInvokeMember(DynamicMetaObject target, } } - // Check if this is a COM Object - DynamicMetaObject result; - if (ComInterop.ComBinder.TryBindInvokeMember(this, _propertySetter, target, args, out result)) - { - return result.UpdateComRestrictionsForPsObject(args).WriteToDebugLog(this); - } - var targetValue = PSObject.Base(target.Value); if (targetValue == null) { diff --git a/src/System.Management.Automation/resources/ParserStrings.resx b/src/System.Management.Automation/resources/ParserStrings.resx index a76527cfbab..b1025946017 100644 --- a/src/System.Management.Automation/resources/ParserStrings.resx +++ b/src/System.Management.Automation/resources/ParserStrings.resx @@ -285,6 +285,12 @@ Missing expression after '{0}'. + + Expected colon after argument name identifier '{0}' but found '{1}'. + + + Expected argument value after name identifier '{0}' but found '{1}'. + ${{variable}} reference starting is missing the closing '}}'. diff --git a/test/powershell/Language/Parser/Parser.Tests.ps1 b/test/powershell/Language/Parser/Parser.Tests.ps1 index e0f90d3a326..5da3faac1ed 100644 --- a/test/powershell/Language/Parser/Parser.Tests.ps1 +++ b/test/powershell/Language/Parser/Parser.Tests.ps1 @@ -1352,4 +1352,156 @@ foo``u{2195}abc $tokens[1] | Should -BeExactly $lastToken } } + + Context "Method invocation argument labels" -Tag Jordan { + It "Parses single argument with label " -TestCases @( + @{ Label = "simple" } + @{ Label = "_underscore" } + @{ Label = "underscore_" } + @{ Label = "a1" } + @{ Label = "_1" } + @{ Label = "`nabc" } + ) { + param ($Label) + + $script = "`$c.Method(${Label}: 'value')" + + $errors = @() + $ast = [System.Management.Automation.Language.Parser]::ParseInput($script, [ref]$null, [ref]$errors) + $errors | Should -BeNullOrEmpty + + $actual = $ast.EndBlock.Statements[0].PipelineElements[0].Expression.Arguments + $actual.Count | Should -Be 1 + $actual | Should -BeOfType ([System.Management.Automation.Language.LabeledExpressionAst]) + $actual.Label | Should -Be $Label.Trim() + $actual.Expression | Should -Be '''value''' + } + + It "Parses single argument with label separated by multiple spaces" { + $script = "`$c.Method(label: 'value')" + + $errors = @() + $ast = [System.Management.Automation.Language.Parser]::ParseInput($script, [ref]$null, [ref]$errors) + $errors | Should -BeNullOrEmpty + + $actual = $ast.EndBlock.Statements[0].PipelineElements[0].Expression.Arguments + $actual.Count | Should -Be 1 + $actual | Should -BeOfType ([System.Management.Automation.Language.LabeledExpressionAst]) + $actual.Label | Should -Be label + $actual.Expression | Should -BeOfType ([System.Management.Automation.Language.StringConstantExpressionAst]) + $actual.Expression.StringConstantType | Should -Be SingleQuoted + $actual.Expression.Value | Should -Be value + } + + It "Parses multiple arguments with labels" { + $script = @' +$c.Method( + $arg1, + label: $arg2, + other: 'string value', + final: "test", + "end") +'@ + + $errors = @() + $ast = [System.Management.Automation.Language.Parser]::ParseInput($script, [ref]$null, [ref]$errors) + $errors | Should -BeNullOrEmpty + + $actual = $ast.EndBlock.Statements[0].PipelineElements[0].Expression.Arguments + $actual.Count | Should -Be 5 + + $actual[0] | Should -BeOfType ([System.Management.Automation.Language.VariableExpressionAst]) + $actual[0].VariablePath | Should -Be arg1 + + $actual[1] | Should -BeOfType ([System.Management.Automation.Language.LabeledExpressionAst]) + $actual[1].Label | Should -Be label + $actual[1].Expression | Should -BeOfType ([System.Management.Automation.Language.VariableExpressionAst]) + $actual[1].Expression.VariablePath | Should -Be arg2 + + $actual[2] | Should -BeOfType ([System.Management.Automation.Language.LabeledExpressionAst]) + $actual[2].Label | Should -Be other + $actual[2].Expression | Should -BeOfType ([System.Management.Automation.Language.StringConstantExpressionAst]) + $actual[2].Expression.StringConstantType | Should -Be SingleQuoted + $actual[2].Expression.Value | Should -Be 'string value' + + $actual[3] | Should -BeOfType ([System.Management.Automation.Language.LabeledExpressionAst]) + $actual[3].Label | Should -Be final + $actual[3].Expression | Should -BeOfType ([System.Management.Automation.Language.StringConstantExpressionAst]) + $actual[3].Expression.StringConstantType | Should -Be DoubleQuoted + $actual[3].Expression.Value | Should -Be test + + $actual[4] | Should -BeOfType ([System.Management.Automation.Language.StringConstantExpressionAst]) + $actual[4].Value | Should -Be end + } + + It "Parses method invocation without label with string value" { + $script = '$c.Method(''value'')' + + $errors = @() + $ast = [System.Management.Automation.Language.Parser]::ParseInput($script, [ref]$null, [ref]$errors) + $errors | Should -BeNullOrEmpty + + $actual = $ast.EndBlock.Statements[0].PipelineElements[0].Expression.Arguments + $actual.Count | Should -Be 1 + + $actual | Should -BeOfType ([System.Management.Automation.Language.StringConstantExpressionAst]) + $actual.StringConstantType | Should -Be SingleQuoted + $actual.value | Should -Be value + } + + It "Parses method invocation without label with provider var" { + $script = '$c.Method($provider:var)' + + $errors = @() + $ast = [System.Management.Automation.Language.Parser]::ParseInput($script, [ref]$null, [ref]$errors) + $errors | Should -BeNullOrEmpty + + $actual = $ast.EndBlock.Statements[0].PipelineElements[0].Expression.Arguments + $actual.Count | Should -Be 1 + + $actual | Should -BeOfType ([System.Management.Automation.Language.VariableExpressionAst]) + $actual.VariablePath.DriveName | Should -Be provider + $actual.VariablePath.UserPath | Should -Be 'provider:var' + } + + It "Fails to parse label with newline after separator" { + $err = { ExecuteCommand "`$c.Method(label:`n'value')" } | Should -Throw -ErrorId ParseException -PassThru + $pe = $err.Exception.InnerException + $pe | Should -BeOfType ([System.Management.Automation.ParseException]) + # $pe.Errors.Count | Should -Be 1 # FIXME + + $pe.Errors[0].ErrorId | Should -Be MissingArgumentAfterLabel + # FIXME: newline not ) + # $pe.Errors[0].Message | Should -Be "Expected argument value after name identifier 'label' but found '``n'." + $pe.ErrorRecord.InvocationInfo.PositionMessage | Should -Be @' +At line:1 char:16 ++ $c.Method(label: ++ ~ +'@ + } + + <# +# Failures to test +# Newline after label +Method(label:`n'value') + +# Non simple name +Method('label': $value) +Method("label": $value) +Method(1abc: $value) +Method(abc def: $value) +Method(café: $value) # non-ASCII char +Method($label: $value) +Method($label : $value) + +# Failure on 2nd arg +Method(label: $value, fail 'value') +Method($value, fail 'value') + +# No : separator +Method(label $value) +Method(label) +Method(label, $value) + #> + } } From 7b0b2346f6a763dc009d56a643759234f60fb459 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Thu, 28 Mar 2024 21:44:55 +1000 Subject: [PATCH 02/29] Expand tests and fix up remaining token parser --- .../engine/parser/Parser.cs | 14 +- .../Language/Parser/Parser.Tests.ps1 | 184 +++++++++++++++--- 2 files changed, 165 insertions(+), 33 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index f0878468ae5..332559a7534 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -7940,11 +7940,23 @@ private List InvokeParamParenListRule(Token lParen, out IScriptEx if (argumentName is not null) { + if (comma is null) + { + comma = NextToken(); + UngetToken(comma); + } + + string nextTokenText = comma.Text; + if (nextTokenText.Length == 1 && char.IsControl(nextTokenText[0])) + { + nextTokenText = string.Format("\\u{0:x4}", (short)nextTokenText[0]); + } + ReportIncompleteInput(After(argumentName.Extent), nameof(ParserStrings.MissingArgumentAfterLabel), ParserStrings.MissingArgumentAfterLabel, argumentName.Value, - comma is null ? TokenKind.RParen.Text() : TokenKind.Comma.Text()); + nextTokenText); reportedError = true; } else if (comma != null) diff --git a/test/powershell/Language/Parser/Parser.Tests.ps1 b/test/powershell/Language/Parser/Parser.Tests.ps1 index 5da3faac1ed..e675ac4d68e 100644 --- a/test/powershell/Language/Parser/Parser.Tests.ps1 +++ b/test/powershell/Language/Parser/Parser.Tests.ps1 @@ -1353,7 +1353,7 @@ foo``u{2195}abc } } - Context "Method invocation argument labels" -Tag Jordan { + Context "Method invocation argument labels" { It "Parses single argument with label " -TestCases @( @{ Label = "simple" } @{ Label = "_underscore" } @@ -1361,6 +1361,7 @@ foo``u{2195}abc @{ Label = "a1" } @{ Label = "_1" } @{ Label = "`nabc" } + @{ Label = "non_ascii_é"} ) { param ($Label) @@ -1468,40 +1469,159 @@ $c.Method( $err = { ExecuteCommand "`$c.Method(label:`n'value')" } | Should -Throw -ErrorId ParseException -PassThru $pe = $err.Exception.InnerException $pe | Should -BeOfType ([System.Management.Automation.ParseException]) - # $pe.Errors.Count | Should -Be 1 # FIXME $pe.Errors[0].ErrorId | Should -Be MissingArgumentAfterLabel - # FIXME: newline not ) - # $pe.Errors[0].Message | Should -Be "Expected argument value after name identifier 'label' but found '``n'." - $pe.ErrorRecord.InvocationInfo.PositionMessage | Should -Be @' -At line:1 char:16 -+ $c.Method(label: -+ ~ -'@ + $pe.Errors[0].Message | Should -Be "Expected argument value after name identifier 'label' but found '\u000a'." + $pe.ErrorRecord.InvocationInfo.PositionMessage | Should -Be (@( + 'At line:1 char:16' + '+ $c.Method(label:' + '+ ~' + ) -join [Environment]::NewLine) + } + + It "Fails to parse label without : separator with value" { + $err = { ExecuteCommand "`$c.Method(label 'value')" } | Should -Throw -ErrorId ParseException -PassThru + $pe = $err.Exception.InnerException + $pe | Should -BeOfType ([System.Management.Automation.ParseException]) + + $pe.Errors[0].ErrorId | Should -Be MissingColonAfterArgumentLabel + $pe.Errors[0].Message | Should -Be "Expected colon after argument name identifier 'label' but found ''value''." + $pe.ErrorRecord.InvocationInfo.PositionMessage | Should -Be (@( + 'At line:1 char:16' + "+ `$c.Method(label 'value')" + '+ ~' + ) -join [Environment]::NewLine) + } + + It "Fails to parse label without : separator with no value" { + $err = { ExecuteCommand "`$c.Method(label)" } | Should -Throw -ErrorId ParseException -PassThru + $pe = $err.Exception.InnerException + $pe | Should -BeOfType ([System.Management.Automation.ParseException]) + + $pe.Errors[0].ErrorId | Should -Be MissingColonAfterArgumentLabel + $pe.Errors[0].Message | Should -Be "Expected colon after argument name identifier 'label' but found ')'." + $pe.ErrorRecord.InvocationInfo.PositionMessage | Should -Be (@( + 'At line:1 char:16' + '+ $c.Method(label)' + '+ ~' + ) -join [Environment]::NewLine) } - <# -# Failures to test -# Newline after label -Method(label:`n'value') - -# Non simple name -Method('label': $value) -Method("label": $value) -Method(1abc: $value) -Method(abc def: $value) -Method(café: $value) # non-ASCII char -Method($label: $value) -Method($label : $value) - -# Failure on 2nd arg -Method(label: $value, fail 'value') -Method($value, fail 'value') - -# No : separator -Method(label $value) -Method(label) -Method(label, $value) - #> + It "Fails to parse label without : separator with no value but extra arg" { + $err = { ExecuteCommand "`$c.Method(label, 'arg')" } | Should -Throw -ErrorId ParseException -PassThru + $pe = $err.Exception.InnerException + $pe | Should -BeOfType ([System.Management.Automation.ParseException]) + + $pe.Errors[0].ErrorId | Should -Be MissingColonAfterArgumentLabel + $pe.Errors[0].Message | Should -Be "Expected colon after argument name identifier 'label' but found ','." + $pe.ErrorRecord.InvocationInfo.PositionMessage | Should -Be (@( + 'At line:1 char:16' + "+ `$c.Method(label, 'arg')" + '+ ~' + ) -join [Environment]::NewLine) + } + + It "Fails to parse label on second argument" { + $err = { ExecuteCommand "`$c.Method(label: `$value, fail 'value')" } | Should -Throw -ErrorId ParseException -PassThru + $pe = $err.Exception.InnerException + $pe | Should -BeOfType ([System.Management.Automation.ParseException]) + + $pe.Errors[0].ErrorId | Should -Be MissingColonAfterArgumentLabel + $pe.Errors[0].Message | Should -Be "Expected colon after argument name identifier 'fail' but found ''value''." + $pe.ErrorRecord.InvocationInfo.PositionMessage | Should -Be (@( + 'At line:1 char:30' + "+ `$c.Method(label: `$value, fail 'value')" + '+ ~' + ) -join [Environment]::NewLine) + } + + It "Fails to parse label on second argument with normal first argument" { + $err = { ExecuteCommand "`$c.Method(`$value, fail 'value')" } | Should -Throw -ErrorId ParseException -PassThru + $pe = $err.Exception.InnerException + $pe | Should -BeOfType ([System.Management.Automation.ParseException]) + + $pe.Errors[0].ErrorId | Should -Be MissingColonAfterArgumentLabel + $pe.Errors[0].Message | Should -Be "Expected colon after argument name identifier 'fail' but found ''value''." + $pe.ErrorRecord.InvocationInfo.PositionMessage | Should -Be (@( + 'At line:1 char:23' + "+ `$c.Method(`$value, fail 'value')" + '+ ~' + ) -join [Environment]::NewLine) + } + + It "Fails to parse quoted label