diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index a32cd48da..596bf137a 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -308,7 +308,16 @@ public async Task GetVariableFromExpression(string variable await debugInfoHandle.WaitAsync(cancellationToken).ConfigureAwait(false); try { - variableList = variables; + // Search the scope containers in order of narrowest to broadest scope (local, + // then script, then global) so that a variable defined in a more local scope + // correctly shadows one of the same name in a parent scope, matching + // PowerShell's own variable resolution. The flattened list of every variable is + // appended as a fallback so that names not present in those scopes (such as + // frame-specific variables) still resolve. See issue #1882. + variableList = new[] { localScopeVariables, scriptScopeVariables, globalScopeVariables } + .Where(container => container is not null) + .SelectMany(container => container.Children.Values) + .Concat(variables); } finally { diff --git a/test/PowerShellEditorServices.Test.Shared/Debugging/VariableScopeTest.ps1 b/test/PowerShellEditorServices.Test.Shared/Debugging/VariableScopeTest.ps1 new file mode 100644 index 000000000..2f6d66e83 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Debugging/VariableScopeTest.ps1 @@ -0,0 +1,5 @@ +$scopeTestVariable = "from parent scope" +& { + $scopeTestVariable = "from local scope" + Write-Output $scopeTestVariable +} diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index 3ba16008d..1d3960140 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -644,6 +644,27 @@ await debugService.SetLineBreakpointsAsync( Assert.False(var.IsExpandable); } + // Regression test for #1882: when a variable of the same name exists in both a parent + // scope and the current (more local) scope, evaluating it via a watch/evaluate request + // must return the most-local value, matching what's shown in the Variables explorer and + // PowerShell's own variable resolution. + [Fact] + public async Task DebuggerResolvesVariableFromMostLocalScope() + { + ScriptFile scopeScriptFile = GetDebugScript("VariableScopeTest.ps1"); + await debugService.SetLineBreakpointsAsync( + scopeScriptFile.FilePath, + new[] { BreakpointDetails.Create(scopeScriptFile.FilePath, 4) }); + + Task _ = ExecuteScriptFileAsync(scopeScriptFile.FilePath); + await AssertDebuggerStopped(scopeScriptFile.FilePath, 4); + + VariableDetailsBase resolved = await debugService.GetVariableFromExpression( + "$scopeTestVariable", CancellationToken.None); + Assert.NotNull(resolved); + Assert.Equal("\"from local scope\"", resolved.ValueString); + } + [Fact] public async Task DebuggerGetsVariables() {