diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index 3de24603421..971e4be4808 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -1620,7 +1620,8 @@ private ScriptBlockAst NamedBlockListRule(Token lCurly, List ? lCurly.Extent : (paramBlockAst != null) ? paramBlockAst.Extent : null; IScriptExtent endExtent = null; - IScriptExtent extent; + IScriptExtent extent = null; + IScriptExtent scriptBlockExtent = null; while (true) { @@ -1628,7 +1629,28 @@ private ScriptBlockAst NamedBlockListRule(Token lCurly, List switch (blockNameToken.Kind) { default: + // Next token is unexpected. + // ErrorRecovery: if 'lCurly' is present, pretend we saw a closing curly; otherwise, eat the unexpected token. + if (lCurly != null) + { + UngetToken(blockNameToken); + scriptBlockExtent = ExtentOf(startExtent, endExtent); + } + else + { + // If "lCurly == null", then it's a ps1/psm1 file, and thus the extent is the whole file. + scriptBlockExtent = _tokenizer.GetScriptExtent(); + } + + // Report error about the unexpected token. + ReportError(blockNameToken.Extent, () => ParserStrings.MissingNamedBlocks, blockNameToken.Text); + goto return_script_block_ast; + + case TokenKind.RCurly: + case TokenKind.EndOfInput: + // If the next token is RCurly or , handle it in 'CompleteScriptBlockBody'. UngetToken(blockNameToken); + extent = ExtentOf(startExtent, endExtent); goto finished_named_block_list; case TokenKind.Dynamicparam: @@ -1687,11 +1709,9 @@ private ScriptBlockAst NamedBlockListRule(Token lCurly, List SkipNewlinesAndSemicolons(); } finished_named_block_list: - - IScriptExtent scriptBlockExtent; - extent = ExtentOf(startExtent, endExtent); CompleteScriptBlockBody(lCurly, ref extent, out scriptBlockExtent); + return_script_block_ast: return new ScriptBlockAst(scriptBlockExtent, usingStatements, paramBlockAst, beginBlock, processBlock, endBlock, dynamicParamBlock); } diff --git a/src/System.Management.Automation/resources/ParserStrings.resx b/src/System.Management.Automation/resources/ParserStrings.resx index 5f9b75345fe..fc98af9421a 100644 --- a/src/System.Management.Automation/resources/ParserStrings.resx +++ b/src/System.Management.Automation/resources/ParserStrings.resx @@ -493,6 +493,9 @@ The correct form is: foreach ($a in $b) {...} Script command clause '{0}' has already been defined. + + unexpected token '{0}', expected 'begin', 'process', 'end', or 'dynamicparam'. + Missing closing '}' in statement block or type definition. diff --git a/test/powershell/Language/Parser/Parser.Tests.ps1 b/test/powershell/Language/Parser/Parser.Tests.ps1 index 0edc62fda66..d0b434af58c 100644 --- a/test/powershell/Language/Parser/Parser.Tests.ps1 +++ b/test/powershell/Language/Parser/Parser.Tests.ps1 @@ -915,4 +915,20 @@ foo``u{2195}abc # Issue #2780 { ExecuteCommand "`$herestr=@`"`n'`"'`n`"@" } | Should Not Throw } + + It "Throw better error when statement should be put in named blocks - " -TestCases @( + @{ script = "Function foo { [CmdletBinding()] param() DynamicParam {} Hi"; name = "function" } + @{ script = "{ begin {} Hi"; name = "script-block" } + @{ script = "begin {} Hi"; name = "script-file" } + ) { + param($script) + + $err = { ExecuteCommand $script } | Should -Throw -ErrorId "ParseException" -PassThru + $err.Exception.InnerException.ErrorRecord.FullyQualifiedErrorId | Should -BeExactly "MissingNamedBlocks" + } + + It "IncompleteParseException should be thrown when only ending curly is missing" { + $err = { ExecuteCommand "Function foo { [CmdletBinding()] param() DynamicParam {} " } | Should -Throw -ErrorId "IncompleteParseException" -PassThru + $err.Exception.InnerException.ErrorRecord.FullyQualifiedErrorId | Should -BeExactly "MissingEndCurlyBrace" + } }