diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs index c6159b1fe8..00b016aa64 100644 --- a/src/System.Management.Automation/engine/parser/Compiler.cs +++ b/src/System.Management.Automation/engine/parser/Compiler.cs @@ -743,14 +743,16 @@ namespace System.Management.Automation.Language s_builtinAttributeGenerator.Add(typeof(ValidateNotNullOrEmptyAttribute), NewValidateNotNullOrEmptyAttribute); } - private Compiler(List sequencePoints) + private Compiler(List sequencePoints, Dictionary sequencePointIndexMap) { _sequencePoints = sequencePoints; + _sequencePointIndexMap = sequencePointIndexMap; } internal Compiler() { _sequencePoints = new List(); + _sequencePointIndexMap = new Dictionary(); } internal bool CompilingConstantExpression { get; set; } @@ -766,6 +768,7 @@ namespace System.Management.Automation.Language private int _switchTupleIndex = VariableAnalysis.Unanalyzed; private int _foreachTupleIndex = VariableAnalysis.Unanalyzed; private readonly List _sequencePoints; + private readonly Dictionary _sequencePointIndexMap; private int _stmtCount; internal bool CompilingMemberFunction { get; set; } @@ -903,17 +906,31 @@ namespace System.Management.Automation.Language ExpressionCache.Constant(version)); } - internal Expression UpdatePosition(Ast ast) + private int AddSequencePoint(IScriptExtent extent) { - _sequencePoints.Add(ast.Extent); + // Make sure we don't add the same extent to the sequence point list twice. + if (!_sequencePointIndexMap.TryGetValue(extent, out int index)) + { + _sequencePoints.Add(extent); + index = _sequencePoints.Count - 1; + _sequencePointIndexMap.Add(extent, index); + } + + return index; + } + + private Expression UpdatePosition(Ast ast) + { + IScriptExtent extent = ast.Extent; + int index = AddSequencePoint(extent); // If we just added the first sequence point, then we don't want to check for breakpoints - we'll do that // in EnterScriptFunction. // Except for while/do loops, in this case we want to check breakpoints on the first sequence point since it // will be executed multiple times. - return ((_sequencePoints.Count == 1) && !_generatingWhileOrDoLoop) + return (index == 0 && !_generatingWhileOrDoLoop) ? ExpressionCache.Empty - : new UpdatePositionExpr(ast.Extent, _sequencePoints.Count - 1, _debugSymbolDocument, !_compilingSingleExpression); + : new UpdatePositionExpr(extent, index, _debugSymbolDocument, !_compilingSingleExpression); } private int _tempCounter; @@ -1683,7 +1700,7 @@ namespace System.Management.Automation.Language // on this sequence point, but it makes it safe to access the CurrentPosition // property in FunctionContext (which can happen if there are exceptions // defining the functions.) - _sequencePoints.Add(ast.Extent); + AddSequencePoint(ast.Extent); } var compileInterpretChoice = (_stmtCount > 300) ? CompileInterpretChoice.NeverCompile : CompileInterpretChoice.CompileOnDemand; @@ -1853,8 +1870,8 @@ namespace System.Management.Automation.Language var exprs = new List(); var temps = new List { _executionContextParameter, LocalVariablesParameter }; GenerateFunctionProlog(exprs, temps, null); - _sequencePoints.Add(expressionAst.Extent); - exprs.Add(new UpdatePositionExpr(expressionAst.Extent, _sequencePoints.Count - 1, _debugSymbolDocument, checkBreakpoints: true)); + int index = AddSequencePoint(expressionAst.Extent); + exprs.Add(new UpdatePositionExpr(expressionAst.Extent, index, _debugSymbolDocument, checkBreakpoints: true)); var result = Compile(expressionAst).Cast(typeof(object)); exprs.Add(Expression.Label(_returnTarget, result)); var body = Expression.Block(new[] { _executionContextParameter, LocalVariablesParameter }, exprs); @@ -2143,7 +2160,7 @@ namespace System.Management.Automation.Language private Tuple, Type> CompileTrap(TrapStatementAst trap) { - var compiler = new Compiler(_sequencePoints) { _compilingTrap = true }; + var compiler = new Compiler(_sequencePoints, _sequencePointIndexMap) { _compilingTrap = true }; string funcName = _currentFunctionName + ""; if (trap.TrapType != null) { @@ -2517,8 +2534,8 @@ namespace System.Management.Automation.Language if (entryExtent != null) { - _sequencePoints.Add(entryExtent); - exprs.Add(new UpdatePositionExpr(entryExtent, _sequencePoints.Count - 1, _debugSymbolDocument, checkBreakpoints: false)); + int index = AddSequencePoint(entryExtent); + exprs.Add(new UpdatePositionExpr(entryExtent, index, _debugSymbolDocument, checkBreakpoints: false)); } exprs.Add( @@ -2568,8 +2585,8 @@ namespace System.Management.Automation.Language } var extent = propertyMember.InitialValue.Extent; - _sequencePoints.Add(extent); - exprs.Add(new UpdatePositionExpr(extent, _sequencePoints.Count - 1, _debugSymbolDocument, checkBreakpoints: false)); + int index = AddSequencePoint(extent); + exprs.Add(new UpdatePositionExpr(extent, index, _debugSymbolDocument, checkBreakpoints: false)); var property = _memberFunctionType.Type.GetProperty(propertyMember.Name, bindingFlags); exprs.Add( Expression.Assign( @@ -2952,12 +2969,12 @@ namespace System.Management.Automation.Language { int clauseCount = ifStmtAst.Clauses.Count; - var exprs = new Tuple[clauseCount]; + var exprs = new Tuple[clauseCount]; for (int i = 0; i < clauseCount; ++i) { IfClause ifClause = ifStmtAst.Clauses[i]; - var cond = Expression.Block( - UpdatePosition(ifClause.Item1), + var cond = UpdatePositionForInitializerOrCondition( + ifClause.Item1, CaptureStatementResults(ifClause.Item1, CaptureAstContext.Condition).Convert(typeof(bool))); var body = Compile(ifClause.Item2); exprs[i] = Tuple.Create(cond, body); @@ -3022,7 +3039,7 @@ namespace System.Management.Automation.Language } var exprs = new List - { + { // Set current position in case of errors. UpdatePosition(assignmentStatementAst), ReduceAssignment((ISupportsAssignment)assignmentStatementAst.Left, @@ -4174,7 +4191,7 @@ namespace System.Management.Automation.Language _loopTargets.Add(new LoopGotoTargets(loopLabel ?? string.Empty, breakLabel, continueLabel)); _generatingWhileOrDoLoop = true; var loopBodyExprs = new List - { + { s_callCheckForInterrupts, Compile(loopStatement.Body), ExpressionCache.Empty @@ -4190,6 +4207,7 @@ namespace System.Management.Automation.Language { test = Expression.Not(test); } + test = UpdatePositionForInitializerOrCondition(loopStatement.Condition, test); exprs.Add(Expression.IfThen(test, Expression.Goto(repeatLabel))); exprs.Add(Expression.Label(breakLabel)); @@ -4240,10 +4258,26 @@ namespace System.Management.Automation.Language // $foreach/$switch = GetEnumerator $enumerable var enumerable = NewTemp(typeof(object), "enumerable"); temps.Add(enumerable); + + // Update position to make it safe to access 'CurrentPosition' property in FunctionContext in case + // that the evaluation of 'stmt.Condition' throws exception. if (generatingForeach) { + // For foreach statement, we want the debugger to stop at 'stmt.Condition' before evaluating it. + // The debugger will stop at 'stmt.Condition' only once. The following enumeration will stop at + // the foreach variable. exprs.Add(UpdatePosition(stmt.Condition)); } + else + { + // For switch statement, we don't want the debugger to stop at 'stmt.Condition' before evaluating it. + // The following enumeration will stop at 'stmt.Condition' again, and we don't want the debugger to + // stop at 'stmt.Condition' twice before getting into one of its case clauses. + var extent = stmt.Condition.Extent; + int index = AddSequencePoint(extent); + exprs.Add(new UpdatePositionExpr(extent, index, _debugSymbolDocument, checkBreakpoints: false)); + } + exprs.Add( Expression.Assign(enumerable, GetRangeEnumerator(stmt.Condition.GetPureExpression()) @@ -4354,6 +4388,19 @@ namespace System.Management.Automation.Language return null; } + private Expression UpdatePositionForInitializerOrCondition(PipelineBaseAst pipelineBaseAst, Expression initializerOrCondition) + { + if (pipelineBaseAst is PipelineAst pipelineAst && !pipelineAst.Background && pipelineAst.GetPureExpression() != null) + { + // If the initializer or condition is a pure expression (CommandExpressionAst without redirection), + // then we need to add a sequence point. If it's an AssignmentStatementAst, we don't need to add + // sequence point here because one will be added when processing the AssignmentStatementAst. + initializerOrCondition = Expression.Block(UpdatePosition(pipelineBaseAst), initializerOrCondition); + } + + return initializerOrCondition; + } + public object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) { return GenerateDoLoop(doWhileStatementAst); @@ -4367,13 +4414,17 @@ namespace System.Management.Automation.Language public object VisitForStatement(ForStatementAst forStatementAst) { // We should not preserve the partial output if exception is thrown when evaluating the initializer. - var init = (forStatementAst.Initializer != null) - ? CaptureStatementResults(forStatementAst.Initializer, CaptureAstContext.AssignmentWithoutResultPreservation) - : null; + Expression init = null; + PipelineBaseAst initializer = forStatementAst.Initializer; + if (initializer != null) + { + init = CaptureStatementResults(initializer, CaptureAstContext.AssignmentWithoutResultPreservation); + init = UpdatePositionForInitializerOrCondition(initializer, init); + } - var generateCondition = forStatementAst.Condition != null - ? () => Expression.Block(UpdatePosition(forStatementAst.Condition), - CaptureStatementResults(forStatementAst.Condition, CaptureAstContext.Condition)) + PipelineBaseAst condition = forStatementAst.Condition; + var generateCondition = condition != null + ? () => UpdatePositionForInitializerOrCondition(condition, CaptureStatementResults(condition, CaptureAstContext.Condition)) : (Func)null; var loop = GenerateWhileLoop(forStatementAst.Label, generateCondition, @@ -4389,9 +4440,9 @@ namespace System.Management.Automation.Language public object VisitWhileStatement(WhileStatementAst whileStatementAst) { + PipelineBaseAst condition = whileStatementAst.Condition; return GenerateWhileLoop(whileStatementAst.Label, - () => Expression.Block(UpdatePosition(whileStatementAst.Condition), - CaptureStatementResults(whileStatementAst.Condition, CaptureAstContext.Condition)), + () => UpdatePositionForInitializerOrCondition(condition, CaptureStatementResults(condition, CaptureAstContext.Condition)), (loopBody, breakTarget, continueTarget) => loopBody.Add(Compile(whileStatementAst.Body))); } diff --git a/test/powershell/Language/Scripting/Debugging/Debugging.Tests.ps1 b/test/powershell/Language/Scripting/Debugging/Debugging.Tests.ps1 index a3d6d05ccd..851e2d5282 100644 --- a/test/powershell/Language/Scripting/Debugging/Debugging.Tests.ps1 +++ b/test/powershell/Language/Scripting/Debugging/Debugging.Tests.ps1 @@ -1,6 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -$script1 = @' + +Describe "Breakpoints when set should be hit" -tag "CI" { + Context "Basic tests" { + BeforeAll { + $script = @' 'aaa'.ToString() > $null 'aa' > $null "a" 2> $null | ForEach-Object { $_ } @@ -8,32 +12,285 @@ $script1 = @' 'bb'.ToSTring() > $null 'bbb' '@ -$script2 = @' -"line 1" -"line 2" -"line 3" + $path = Setup -PassThru -File BasicTest.ps1 -Content $script + $bps = 1..6 | ForEach-Object { set-psbreakpoint -script $path -line $_ -Action { continue } } + } + + AfterAll { + $bps | Remove-PSBreakPoint + } + + It "A redirected breakpoint is hit" { + & $path + foreach ( $bp in $bps ) { + $bp.HitCount | Should -Be 1 + } + } + } + + Context "Break point on switch condition should be hit only when enumerating it" { + BeforeAll { + $script = @' +$test = 1..2 +switch ($test) +{ + default {} +} +'@ + $path = Setup -PassThru -File SwitchScript.ps1 -Content $script + $breakpoint = Set-PSBreakpoint -Script $path -Line 2 -Action { continue } + } + + AfterAll { + Remove-PSBreakpoint -Breakpoint $breakpoint + } + + It "switch condition should be hit 3 times" { + ## MoveNext() will be called on the condition for 3 times + $null = & $path + $breakpoint.HitCount | Should -Be 3 + } + } + + Context "Break point on for-statement initializer should be hit" { + BeforeAll { + $for_script_1 = @' +$test = 2 +for ("string".Length; + $test -gt 0; $test--) { } '@ -Describe "Breakpoints when set should be hit" -tag "CI" { - BeforeAll { - $path = setup -pass -f TestScript_1.ps1 -content $script1 - $bps = 1..6 | ForEach-Object { set-psbreakpoint -script $path -line $_ -Action { continue } } + $for_script_2 = @' +$test = $PSCommandPath +for (Test-Path $test; + $test -eq "blah";) { } +'@ + + $for_script_3 = @' +for ($test = 2; + $test -gt 0; $test--) { } +'@ + + $for_script_4 = @' +$test = 2 +for (;$test -gt 0; + $test--) { } +'@ + + $for_script_5 = @' +$test = $PSCommandPath +for (;Test-Path $test;) +{ + $test = "blah" +} +'@ + $ForScript_1 = Setup -PassThru -File ForScript_1.ps1 -Content $for_script_1 + $bp_1 = Set-PSBreakpoint -Script $ForScript_1 -Line 2 -Action { continue } + + $ForScript_2 = Setup -PassThru -File ForScript_2.ps1 -Content $for_script_2 + $bp_2 = Set-PSBreakpoint -Script $ForScript_2 -Line 2 -Action { continue } + + $ForScript_3 = Setup -PassThru -File ForScript_3.ps1 -Content $for_script_3 + $bp_3 = Set-PSBreakpoint -Script $ForScript_3 -Line 1 -Action { continue } + + $ForScript_4 = Setup -PassThru -File ForScript_4.ps1 -Content $for_script_4 + $bp_4 = Set-PSBreakpoint -Script $ForScript_4 -Line 2 -Action { continue } + + $ForScript_5 = Setup -PassThru -File ForScript_5.ps1 -Content $for_script_5 + $bp_5 = Set-PSBreakpoint -Script $ForScript_5 -Line 2 -Action { continue } + + $testCases = @( + @{ Name = "expression initializer should be hit once"; Path = $ForScript_1; Breakpoint = $bp_1; HitCount = 1 } + @{ Name = "pipeline initializer should be hit once"; Path = $ForScript_2; Breakpoint = $bp_2; HitCount = 1 } + @{ Name = "assignment initializer should be hit 3 times"; Path = $ForScript_3; Breakpoint = $bp_3; HitCount = 1 } + @{ Name = "pipeline condition should be hit 3 times"; Path = $ForScript_4; Breakpoint = $bp_4; HitCount = 3 } + @{ Name = "pipeline condition should be hit 2 times"; Path = $ForScript_5; Breakpoint = $bp_5; HitCount = 2 } + ) + } + + AfterAll { + Get-PSBreakpoint -Script $ForScript_1, $ForScript_2, $ForScript_3, $ForScript_4, $ForScript_5 | Remove-PSBreakpoint + } + + It "for-statement " -TestCases $testCases { + param($Path, $Breakpoint, $HitCount) + $null = & $Path + $Breakpoint.HitCount | Should -Be $HitCount + } } - AfterAll { - $bps | Remove-PSBreakPoint + + Context "Break point on while loop condition should be hit" { + BeforeAll { + $while_script_1 = @' +$test = "string" +while ($test.Contains("str")) +{ + $test = "blah" +} +'@ + + $while_script_2 = @' +$test = $PSCommandPath +while (Test-Path $test) +{ + $test = "blah" +} +'@ + $WhileScript_1 = Setup -PassThru -File WhileScript_1.ps1 -Content $while_script_1 + $bp_1 = Set-PSBreakpoint -Script $WhileScript_1 -Line 2 -Action { continue } + + $WhileScript_2 = Setup -PassThru -File WhileScript_2.ps1 -Content $while_script_2 + $bp_2 = Set-PSBreakpoint -Script $WhileScript_2 -Line 2 -Action { continue } + + $testCases = @( + @{ Name = "expression condition should be hit 2 times"; Path = $WhileScript_1; Breakpoint = $bp_1; HitCount = 2 } + @{ Name = "pipeline condition should be hit 2 times"; Path = $WhileScript_2; Breakpoint = $bp_2; HitCount = 2 } + ) + } + + AfterAll { + Get-PSBreakpoint -Script $WhileScript_1, $WhileScript_2 | Remove-PSBreakpoint + } + + It "while loop " -TestCases $testCases { + param($Path, $Breakpoint, $HitCount) + $null = & $Path + $Breakpoint.HitCount | Should -Be $HitCount + } } - It "A redirected breakpoint is hit" { - & $path - foreach ( $bp in $bps ) { - $bp.HitCount | Should -Be 1 + + Context "Break point on do-while loop condition should be hit" { + BeforeAll { + $do_while_script_1 = @' +$test = "blah" +do { echo $test } +while ($test.Contains("str")) +'@ + + $do_while_script_2 = @' +$test = "blah" +do { echo $test } +while (Test-Path $test) +'@ + $DoWhileScript_1 = Setup -PassThru -File DoWhileScript_1.ps1 -Content $do_while_script_1 + $bp_1 = Set-PSBreakpoint -Script $DoWhileScript_1 -Line 2 -Action { continue } + + $DoWhileScript_2 = Setup -PassThru -File DoWhileScript_2.ps1 -Content $do_while_script_2 + $bp_2 = Set-PSBreakpoint -Script $DoWhileScript_2 -Line 2 -Action { continue } + + $testCases = @( + @{ Name = "expression condition should be hit 2 times"; Path = $DoWhileScript_1; Breakpoint = $bp_1; HitCount = 1 } + @{ Name = "pipeline condition should be hit 2 times"; Path = $DoWhileScript_2; Breakpoint = $bp_2; HitCount = 1 } + ) + } + + AfterAll { + Get-PSBreakpoint -Script $DoWhileScript_1, $DoWhileScript_2 | Remove-PSBreakpoint + } + + It "Do-While loop " -TestCases $testCases { + param($Path, $Breakpoint, $HitCount) + $null = & $Path + $Breakpoint.HitCount | Should -Be $HitCount + } + } + + Context "Break point on do-until loop condition should be hit" { + BeforeAll { + $do_until_script_1 = @' +$test = "blah" +do { echo $test } +until ($test.Contains("bl")) +'@ + + $do_until_script_2 = @' +$test = $PSCommandPath +do { echo $test } +until (Test-Path $test) +'@ + $DoUntilScript_1 = Setup -PassThru -File DoUntilScript_1.ps1 -Content $do_until_script_1 + $bp_1 = Set-PSBreakpoint -Script $DoUntilScript_1 -Line 2 -Action { continue } + + $DoUntilScript_2 = Setup -PassThru -File DoUntilScript_2.ps1 -Content $do_until_script_2 + $bp_2 = Set-PSBreakpoint -Script $DoUntilScript_2 -Line 2 -Action { continue } + + $testCases = @( + @{ Name = "expression condition should be hit 2 times"; Path = $DoUntilScript_1; Breakpoint = $bp_1; HitCount = 1 } + @{ Name = "pipeline condition should be hit 2 times"; Path = $DoUntilScript_2; Breakpoint = $bp_2; HitCount = 1 } + ) + } + + AfterAll { + Get-PSBreakpoint -Script $DoUntilScript_1, $DoUntilScript_2 | Remove-PSBreakpoint + } + + It "Do-Until loop " -TestCases $testCases { + param($Path, $Breakpoint, $HitCount) + $null = & $Path + $Breakpoint.HitCount | Should -Be $HitCount + } + } + + Context "Break point on if condition should be hit" { + BeforeAll { + $if_script_1 = @' +if ("string".Contains('str')) +{ } +'@ + $if_script_2 = @' +if (Test-Path $PSCommandPath) +{ } +'@ + $if_script_3 = @' +if ($false) {} +elseif ("string".Contains('str')) +{ } +'@ + $if_script_4 = @' +if ($false) {} +elseif (Test-Path $PSCommandPath) +{ } +'@ + $IfScript_1 = Setup -PassThru -File IfScript_1.ps1 -Content $if_script_1 + $bp_1 = Set-PSBreakpoint -Script $IfScript_1 -Line 1 -Action { continue } + + $IfScript_2 = Setup -PassThru -File IfScript_2.ps1 -Content $if_script_2 + $bp_2 = Set-PSBreakpoint -Script $IfScript_2 -Line 1 -Action { continue } + + $IfScript_3 = Setup -PassThru -File IfScript_3.ps1 -Content $if_script_3 + $bp_3 = Set-PSBreakpoint -Script $IfScript_3 -Line 2 -Action { continue } + + $IfScript_4 = Setup -PassThru -File IfScript_4.ps1 -Content $if_script_4 + $bp_4 = Set-PSBreakpoint -Script $IfScript_4 -Line 2 -Action { continue } + + $testCases = @( + @{ Name = "expression if-condition should be hit once"; Path = $IfScript_1; Breakpoint = $bp_1; HitCount = 1 } + @{ Name = "pipeline if-condition should be hit once"; Path = $IfScript_2; Breakpoint = $bp_2; HitCount = 1 } + @{ Name = "expression elseif-condition should be hit once"; Path = $IfScript_3; Breakpoint = $bp_3; HitCount = 1 } + @{ Name = "pipeline elseif-condition should be hit once"; Path = $IfScript_4; Breakpoint = $bp_4; HitCount = 1 } + ) + } + + AfterAll { + Get-PSBreakpoint -Script $IfScript_1, $IfScript_2, $IfScript_3, $IfScript_4 | Remove-PSBreakpoint + } + + It "If statement " -TestCases $testCases { + param($Path, $Breakpoint, $HitCount) + $null = & $Path + $Breakpoint.HitCount | Should -Be $HitCount } } } Describe "It should be possible to reset runspace debugging" -tag "Feature" { BeforeAll { - $path = setup -pass -f TestScript_2.ps1 -content $script2 - $scriptPath = "$testdrive/TestScript_2.ps1" + $script = @' +"line 1" +"line 2" +"line 3" +'@ + $scriptPath = Setup -PassThru -File TestScript.ps1 -Content $script $iss = [initialsessionstate]::CreateDefault2(); $rs = [runspacefactory]::CreateRunspace($iss) $rs.Name = "TestRunspaceDebuggerReset" @@ -87,6 +344,6 @@ Describe "It should be possible to reset runspace debugging" -tag "Feature" { } It "The script should run without a break" { $ps.Commands.Clear() - $ps.addscript("$testdrive/TestScript_2.ps1").Invoke().Count | Should -Be 3 + $ps.addscript($scriptPath).Invoke().Count | Should -Be 3 } } diff --git a/test/powershell/Language/Scripting/ErrorPosition.Tests.ps1 b/test/powershell/Language/Scripting/ErrorPosition.Tests.ps1 new file mode 100644 index 0000000000..62777bda32 --- /dev/null +++ b/test/powershell/Language/Scripting/ErrorPosition.Tests.ps1 @@ -0,0 +1,109 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Describe "Error position Tests" -Tags "CI" { + + BeforeAll { + $switch_condition_script = @' +$test = 1 +switch ($null[0]) { + "a" {}; +} +'@ + $for_expression_initializer_script = @' +$test = 1 +for ($null[0]; + $test -gt 1;) { } +'@ + $for_pipeline_initializer_script = @' +$test = 1 +for (Test-Path $null; + $test -gt 1;) { } +'@ + $for_expression_condition_script = @' +$test = 1 +for (;$null[0];) +{ } +'@ + $for_pipeline_condition_script = @' +$test = 1 +for (;Test-Path $null;) +{ } +'@ + $do_while_expression_condition_script = @' +$test = 1 +do {} +while ($null[0]) +'@ + $do_while_pipeline_condition_script = @' +$test = 1 +do {} +while (Test-Path $null) +'@ + $do_until_expression_condition_script = @' +$test = 1 +do {} +until ($null[0]) +'@ + $do_until_pipeline_condition_script = @' +$test = 1 +do {} +until (Test-Path $null) +'@ + + $testCases = @( + @{ Name = "switch condition"; FileName = "SwitchError2.ps1"; Script = $switch_condition_script; MatchText = "SwitchError2.ps1: line 2" } + @{ Name = "for statement expression initializer"; FileName = "ForError1.ps1"; Script = $for_expression_initializer_script; MatchText = "ForError1.ps1: line 2" } + @{ Name = "for statement pipeline initializer"; FileName = "ForError2.ps1"; Script = $for_pipeline_initializer_script; MatchText = "ForError2.ps1: line 2" } + @{ Name = "for statement expression condition"; FileName = "ForError3.ps1"; Script = $for_expression_condition_script; MatchText = "ForError3.ps1: line 2" } + @{ Name = "for statement pipeline condition"; FileName = "ForError4.ps1"; Script = $for_pipeline_condition_script; MatchText = "ForError4.ps1: line 2" } + @{ Name = "do-while statement expression condition"; FileName = "DoWhileError1.ps1"; Script = $do_while_expression_condition_script; MatchText = "DoWhileError1.ps1: line 3" } + @{ Name = "do-while statement pipeline condition"; FileName = "DoWhileError2.ps1"; Script = $do_while_pipeline_condition_script; MatchText = "DoWhileError2.ps1: line 3" } + @{ Name = "do-until statement expression condition"; FileName = "DoUntilError1.ps1"; Script = $do_until_expression_condition_script; MatchText = "DoUntilError1.ps1: line 3" } + @{ Name = "do-until statement pipeline condition"; FileName = "DoUntilError2.ps1"; Script = $do_until_pipeline_condition_script; MatchText = "DoUntilError2.ps1: line 3" } + ) + } + + It " evaluation failure should report correct error position" -TestCases $testCases { + param($FileName, $Script, $MatchText) + $testFile = Join-Path $TestDrive $FileName + Set-Content -Path $testFile -Encoding Ascii -Value $Script + try { & $testFile } catch { $errorRecord = $_ } + $errorRecord | Should -Not -BeNullOrEmpty + $errorRecord.ScriptStackTrace | Should -Match $MatchText + } + + It "switch condition MoveNext failure should report correct error position" { + $code = @' +using System; +using System.Collections.Generic; +namespace SwitchTest +{ + public class Test + { + public static IEnumerable GetName() + { + yield return "Hello world"; + throw new ArgumentException(); + } + } +} +'@ + $testFile = Join-Path $TestDrive "SwitchError1.ps1" + Set-Content -Path $testFile -Encoding Ascii -Value @' +$test = 1 +$enumerable = [SwitchTest.Test]::GetName() +switch ($enumerable) { + "hello world" { $test = 1; $_ } + "Yay" { $test = 2; $_ } +} +'@ + if (-not ("SwitchTest.Test" -as [type])) { + Add-Type -TypeDefinition $code + } + + try { & $testFile > $null } catch { $errorRecord = $_ } + $errorRecord | Should -Not -BeNullOrEmpty + $errorRecord.ScriptStackTrace | Should -Match "SwitchError1.ps1: line 3" + } +}