Skip to content

Commit 27e2d2f

Browse files
Add artificial stack frame to represent contexts without one (#1898)
* Add artifical stack frame to represent context When stepping into certain contexts like a param block's default value expression, the engine does not provide a call stack frame to represent it. In this scenario we want to create an artifical call stack frame to represent the context we've stepped into. * Add doc comments to new `PathUtils` members
1 parent 75111b6 commit 27e2d2f

File tree

3 files changed

+78
-7
lines changed

3 files changed

+78
-7
lines changed

src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs

+39-4
Original file line numberDiff line numberDiff line change
@@ -994,10 +994,45 @@ await _remoteFileManager.FetchRemoteFileAsync(
994994
// Augment the top stack frame with details from the stop event
995995
if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent)
996996
{
997-
stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber;
998-
stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber;
999-
stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber;
1000-
stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber;
997+
StackFrameDetails targetFrame = stackFrameDetails[0];
998+
999+
// Certain context changes (like stepping into the default value expression
1000+
// of a parameter) do not create a call stack frame. In order to represent
1001+
// this context change we create a fake call stack frame.
1002+
if (!string.IsNullOrEmpty(scriptExtent.File)
1003+
&& !PathUtils.IsPathEqual(scriptExtent.File, targetFrame.ScriptPath))
1004+
{
1005+
await debugInfoHandle.WaitAsync().ConfigureAwait(false);
1006+
try
1007+
{
1008+
targetFrame = new StackFrameDetails
1009+
{
1010+
ScriptPath = scriptExtent.File,
1011+
// Just use the last frame's variables since we don't have a
1012+
// good way to get real values.
1013+
AutoVariables = targetFrame.AutoVariables,
1014+
CommandVariables = targetFrame.CommandVariables,
1015+
// Ideally we'd get a real value here but since there's no real
1016+
// call stack frame for this, we'd need to replicate a lot of
1017+
// engine code.
1018+
FunctionName = "<ScriptBlock>",
1019+
};
1020+
1021+
StackFrameDetails[] newFrames = new StackFrameDetails[stackFrameDetails.Length + 1];
1022+
newFrames[0] = targetFrame;
1023+
stackFrameDetails.CopyTo(newFrames, 1);
1024+
stackFrameDetails = newFrames;
1025+
}
1026+
finally
1027+
{
1028+
debugInfoHandle.Release();
1029+
}
1030+
}
1031+
1032+
targetFrame.StartLineNumber = scriptExtent.StartLineNumber;
1033+
targetFrame.EndLineNumber = scriptExtent.EndLineNumber;
1034+
targetFrame.StartColumnNumber = scriptExtent.StartColumnNumber;
1035+
targetFrame.EndColumnNumber = scriptExtent.EndColumnNumber;
10011036
}
10021037
}
10031038

src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ internal class StackFrameDetails
3131
/// <summary>
3232
/// Gets the name of the function where the stack frame occurred.
3333
/// </summary>
34-
public string FunctionName { get; private set; }
34+
public string FunctionName { get; internal init; }
3535

3636
/// <summary>
3737
/// Gets the start line number of the script where the stack frame occurred.
@@ -62,12 +62,12 @@ internal class StackFrameDetails
6262
/// <summary>
6363
/// Gets or sets the VariableContainerDetails that contains the auto variables.
6464
/// </summary>
65-
public VariableContainerDetails AutoVariables { get; private set; }
65+
public VariableContainerDetails AutoVariables { get; internal init; }
6666

6767
/// <summary>
6868
/// Gets or sets the VariableContainerDetails that contains the call stack frame variables.
6969
/// </summary>
70-
public VariableContainerDetails CommandVariables { get; private set; }
70+
public VariableContainerDetails CommandVariables { get; internal init; }
7171

7272
#endregion
7373

src/PowerShellEditorServices/Utility/PathUtils.cs

+36
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System;
45
using System.IO;
56
using System.Management.Automation;
67
using System.Runtime.InteropServices;
@@ -29,13 +30,48 @@ internal static class PathUtils
2930
/// </summary>
3031
internal static readonly char AlternatePathSeparator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '/' : '\\';
3132

33+
/// <summary>
34+
/// The <see cref="StringComparison" /> value to be used when comparing paths. Will be
35+
/// <see cref="StringComparison.Ordinal" /> for case sensitive file systems and <see cref="StringComparison.OrdinalIgnoreCase" />
36+
/// in case insensitive file systems.
37+
/// </summary>
38+
internal static readonly StringComparison PathComparison = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
39+
? StringComparison.Ordinal
40+
: StringComparison.OrdinalIgnoreCase;
41+
3242
/// <summary>
3343
/// Converts all alternate path separators to the current platform's main path separators.
3444
/// </summary>
3545
/// <param name="path">The path to normalize.</param>
3646
/// <returns>The normalized path.</returns>
3747
public static string NormalizePathSeparators(string path) => string.IsNullOrWhiteSpace(path) ? path : path.Replace(AlternatePathSeparator, DefaultPathSeparator);
3848

49+
/// <summary>
50+
/// Determines whether two specified strings represent the same path.
51+
/// </summary>
52+
/// <param name="left">The first path to compare, or <see langword="null" />.</param>
53+
/// <param name="right">The second path to compare, or <see langword="null" />.</param>
54+
/// <returns>
55+
/// <see langword="true" /> if the value of <paramref name="left" /> represents the same
56+
/// path as the value of <paramref name="right" />; otherwise, <see langword="false" />.
57+
/// </returns>
58+
internal static bool IsPathEqual(string left, string right)
59+
{
60+
if (string.IsNullOrEmpty(left))
61+
{
62+
return string.IsNullOrEmpty(right);
63+
}
64+
65+
if (string.IsNullOrEmpty(right))
66+
{
67+
return false;
68+
}
69+
70+
left = Path.GetFullPath(left).TrimEnd(DefaultPathSeparator);
71+
right = Path.GetFullPath(right).TrimEnd(DefaultPathSeparator);
72+
return left.Equals(right, PathComparison);
73+
}
74+
3975
/// <summary>
4076
/// Return the given path with all PowerShell globbing characters escaped,
4177
/// plus optionally the whitespace.

0 commit comments

Comments
 (0)