* Fixing 2 variables with same names in different scopes in a async method.
* Implementing support to see local variables in async methods in VB code, and also fixing 2 variables with same name in differente scopes, and also fixing method name for async methods in VB.
* Adding Test case as suggested by @radical
* Refactor Scope class
* slight test changes
* Split the tests, to be more readable in code, and in test results
* cleanup
* addressing radical comments
* addressing radical comments, fix test case
* addressing radical comments
* addressing @radical comments
Co-authored-by: Ankit Jain <radical@gmail.com>
private ParameterInfo[] _parametersInfo;
public int KickOffMethod { get; }
internal bool IsCompilerGenerated { get; }
+ private readonly AsyncScopeDebugInformation[] _asyncScopes;
public MethodInfo(AssemblyInfo assembly, string methodName, int methodToken, TypeInfo type, MethodAttributes attrs)
{
this.TypeInfo = type;
TypeInfo.Methods.Add(this);
assembly.Methods[methodToken] = this;
+ _asyncScopes = Array.Empty<AsyncScopeDebugInformation>();
}
public MethodInfo(AssemblyInfo assembly, MethodDefinitionHandle methodDefHandle, int token, SourceFile source, TypeInfo type, MetadataReader asmMetadataReader, MetadataReader pdbMetadataReader)
DebuggerAttrInfo.ClearInsignificantAttrFlags();
}
if (pdbMetadataReader != null)
+ {
localScopes = pdbMetadataReader.GetLocalScopes(methodDefHandle);
+ byte[] scopeDebugInformation =
+ (from cdiHandle in pdbMetadataReader.GetCustomDebugInformation(methodDefHandle)
+ let cdi = pdbMetadataReader.GetCustomDebugInformation(cdiHandle)
+ where pdbMetadataReader.GetGuid(cdi.Kind) == PortableCustomDebugInfoKinds.StateMachineHoistedLocalScopes
+ select pdbMetadataReader.GetBlobBytes(cdi.Value)).FirstOrDefault();
+
+ if (scopeDebugInformation != null)
+ {
+ _asyncScopes = new AsyncScopeDebugInformation[scopeDebugInformation.Length / 8];
+ for (int i = 0; i < _asyncScopes.Length; i++)
+ {
+ int scopeOffset = BitConverter.ToInt32(scopeDebugInformation, i * 8);
+ int scopeLen = BitConverter.ToInt32(scopeDebugInformation, (i * 8) + 4);
+ _asyncScopes[i] = new AsyncScopeDebugInformation(scopeOffset, scopeOffset + scopeLen);
+ }
+ }
+
+ _asyncScopes ??= Array.Empty<AsyncScopeDebugInformation>();
+ }
+ }
+
+ public bool ContainsAsyncScope(int oneBasedIdx, int offset)
+ {
+ int arrIdx = oneBasedIdx - 1;
+ return arrIdx >= 0 && arrIdx < _asyncScopes.Length &&
+ offset >= _asyncScopes[arrIdx].StartOffset && offset <= _asyncScopes[arrIdx].EndOffset;
}
public ParameterInfo[] GetParametersInfo()
return loc.Source.Id;
}
}
+
+ private record struct AsyncScopeDebugInformation(int StartOffset, int EndOffset);
}
internal sealed class ParameterInfo
VarInfo[] varIds = scope.Method.Info.GetLiveVarsAt(scope.Location.IlLocation.Offset);
- var values = await context.SdbAgent.StackFrameGetValues(scope.Method, context.ThreadId, scopeId, varIds, token);
+ var values = await context.SdbAgent.StackFrameGetValues(scope.Method, context.ThreadId, scopeId, varIds, scope.Location.IlLocation.Offset, token);
if (values != null)
{
if (values == null || values.Count == 0)
private SessionId sessionId;
internal readonly ILogger logger;
- private static readonly Regex regexForAsyncLocals = new (@"\<([^)]*)\>", RegexOptions.Singleline);
+ private static readonly Regex regexForAsyncLocals = new(@"\<([^)]*)\>([^)]*)([_][_])([0-9]*)", RegexOptions.Singleline); //<testCSharpScope>5__1
+ private static readonly Regex regexForVBAsyncLocals = new(@"\$VB\$ResumableLocal_([^)]*)\$([0-9]*)", RegexOptions.Singleline); //$VB$ResumableLocal_testVbScope$2
+ private static readonly Regex regexForVBAsyncMethodName = new(@"VB\$StateMachine_([0-9]*)_([^)]*)", RegexOptions.Singleline); //VB$StateMachine_2_RunVBScope
private static readonly Regex regexForAsyncMethodName = new (@"\<([^>]*)\>([d][_][_])([0-9]*)", RegexOptions.Compiled);
private static readonly Regex regexForGenericArgs = new (@"[`][0-9]+", RegexOptions.Compiled);
private static readonly Regex regexForNestedLeftRightAngleBrackets = new ("^(((?'Open'<)[^<>]*)+((?'Close-Open'>)[^<>]*)+)*(?(Open)(?!))[^<>]*", RegexOptions.Compiled);
if (anonymousMethodId.LastIndexOf('_') >= 0)
anonymousMethodId = klassName.Substring(klassName.LastIndexOf('_') + 1);
}
+ else if (klassName.StartsWith("VB$"))
+ {
+ var match = regexForVBAsyncMethodName.Match(klassName);
+ if (match.Success)
+ ret = ret.Insert(0, match.Groups[2].Value);
+ else
+ ret = ret.Insert(0, klassName);
+ }
else
{
var matchOnClassName = regexForNestedLeftRightAngleBrackets.Match(klassName);
fieldName.StartsWith ("<>8__", StringComparison.Ordinal);
}
- public async Task<JArray> GetHoistedLocalVariables(int objectId, IEnumerable<JToken> asyncLocals, CancellationToken token)
+ public async Task<JArray> GetHoistedLocalVariables(MethodInfoWithDebugInformation method, int objectId, IEnumerable<JToken> asyncLocals, int offset, CancellationToken token)
{
JArray asyncLocalsFull = new JArray();
List<int> objectsAlreadyRead = new();
if (fieldName.EndsWith("__this", StringComparison.Ordinal))
{
asyncLocal["name"] = "this";
- asyncLocalsFull.Add(asyncLocal);
}
else if (IsClosureReferenceField(fieldName)) //same code that has on debugger-libs
{
{
var asyncProxyMembersFromObject = await MemberObjectsExplorer.GetObjectMemberValues(
this, dotnetObjectId.Value, GetObjectCommandOptions.WithProperties, token);
- var hoistedLocalVariable = await GetHoistedLocalVariables(dotnetObjectId.Value, asyncProxyMembersFromObject.Flatten(), token);
+ var hoistedLocalVariable = await GetHoistedLocalVariables(method, dotnetObjectId.Value, asyncProxyMembersFromObject.Flatten(), offset, token);
asyncLocalsFull = new JArray(asyncLocalsFull.Union(hoistedLocalVariable));
}
}
+ continue;
}
else if (fieldName.StartsWith("<>", StringComparison.Ordinal)) //examples: <>t__builder, <>1__state
{
{
var match = regexForAsyncLocals.Match(fieldName);
if (match.Success)
+ {
+ if (!method.Info.ContainsAsyncScope(Convert.ToInt32(match.Groups[4].Value), offset))
+ continue;
asyncLocal["name"] = match.Groups[1].Value;
- asyncLocalsFull.Add(asyncLocal);
+ }
}
- else
+ //VB language
+ else if (fieldName.StartsWith("$VB$Local_", StringComparison.Ordinal))
+ {
+ asyncLocal["name"] = fieldName.Remove(0, 10);
+ }
+ else if (fieldName.StartsWith("$VB$ResumableLocal_", StringComparison.Ordinal))
{
- asyncLocalsFull.Add(asyncLocal);
+ var match = regexForVBAsyncLocals.Match(fieldName);
+ if (match.Success)
+ {
+ if (!method.Info.ContainsAsyncScope(Convert.ToInt32(match.Groups[2].Value) + 1, offset))
+ continue;
+ asyncLocal["name"] = match.Groups[1].Value;
+ }
+ }
+ else if (fieldName.StartsWith("$"))
+ {
+ continue;
}
+ asyncLocalsFull.Add(asyncLocal);
}
return asyncLocalsFull;
}
- public async Task<JArray> StackFrameGetValues(MethodInfoWithDebugInformation method, int thread_id, int frame_id, VarInfo[] varIds, CancellationToken token)
+ public async Task<JArray> StackFrameGetValues(MethodInfoWithDebugInformation method, int thread_id, int frame_id, VarInfo[] varIds, int offset, CancellationToken token)
{
using var commandParamsWriter = new MonoBinaryWriter();
commandParamsWriter.Write(thread_id);
retDebuggerCmdReader.ReadByte(); //ignore type
var objectId = retDebuggerCmdReader.ReadInt32();
GetMembersResult asyncProxyMembers = await MemberObjectsExplorer.GetObjectMemberValues(this, objectId, GetObjectCommandOptions.WithProperties, token, includeStatic: true);
- var asyncLocals = await GetHoistedLocalVariables(objectId, asyncProxyMembers.Flatten(), token);
+ var asyncLocals = await GetHoistedLocalVariables(method, objectId, asyncProxyMembers.Flatten(), offset, token);
return asyncLocals;
}
ncs_dt0 = TDateTime(new DateTime(3412, 4, 6, 8, 0, 2))
}, "locals");
});
+
+ [Theory]
+ [InlineData("Run", 246, 16, 252, 16, "RunCSharpScope")]
+ [InlineData("RunContinueWith", 277, 20, 283, 20, "RunContinueWithSameVariableName")]
+ [InlineData("RunNestedContinueWith", 309, 24, 315, 24, "RunNestedContinueWithSameVariableName.AnonymousMethod__1")]
+ [InlineData("RunNonAsyncMethod", 334, 16, 340, 16, "RunNonAsyncMethodSameVariableName")]
+ public async Task InspectLocalsWithSameNameInDifferentScopesInAsyncMethod_CSharp(string method_to_run, int line1, int col1, int line2, int col2, string func_to_pause)
+ => await InspectLocalsWithSameNameInDifferentScopesInAsyncMethod(
+ $"[debugger-test] DebuggerTests.AsyncTests.VariablesWithSameNameDifferentScopes:{method_to_run}",
+ "dotnet://debugger-test.dll/debugger-async-test.cs",
+ line1,
+ col1,
+ line2,
+ col2,
+ $"DebuggerTests.AsyncTests.VariablesWithSameNameDifferentScopes.{func_to_pause}",
+ "testCSharpScope");
+
+ [Theory]
+ [InlineData("[debugger-test-vb] DebuggerTestVB.TestVbScope:Run", 14, 12, 22, 12, "DebuggerTestVB.TestVbScope.RunVBScope", "testVbScope")]
+ public async Task InspectLocalsWithSameNameInDifferentScopesInAsyncMethod_VB(string method_to_run, int line1, int col1, int line2, int col2, string func_to_pause, string variable_to_inspect)
+ => await InspectLocalsWithSameNameInDifferentScopesInAsyncMethod(
+ method_to_run,
+ "dotnet://debugger-test-vb.dll/debugger-test-vb.vb",
+ line1,
+ col1,
+ line2,
+ col2,
+ func_to_pause,
+ variable_to_inspect);
+
+ private async Task InspectLocalsWithSameNameInDifferentScopesInAsyncMethod(string method_to_run, string source_to_pause, int line1, int col1, int line2, int col2, string func_to_pause, string variable_to_inspect)
+ {
+ var expression = $"{{ invoke_static_method('{method_to_run}'); }}";
+
+ await EvaluateAndCheck(
+ "window.setTimeout(function() {" + expression + "; }, 1);",
+ source_to_pause, line1, col1,
+ func_to_pause,
+ locals_fn: async (locals) =>
+ {
+ await CheckString(locals, variable_to_inspect, "hello");
+ await CheckString(locals, "onlyInFirstScope", "only-in-first-scope");
+ Assert.False(locals.Any(jt => jt["name"]?.Value<string>() == "onlyInSecondScope"));
+ }
+ );
+ await StepAndCheck(StepKind.Resume, source_to_pause, line2, col2, func_to_pause,
+ locals_fn: async (locals) =>
+ {
+ await CheckString(locals, variable_to_inspect, "hi");
+ await CheckString(locals, "onlyInSecondScope", "only-in-second-scope");
+ Assert.False(locals.Any(jt => jt["name"]?.Value<string>() == "onlyInFirstScope"));
+ }
+ );
+ }
+
+ [Fact]
+ public async Task InspectLocalsInAsyncVBMethod()
+ {
+ var expression = $"{{ invoke_static_method('[debugger-test-vb] DebuggerTestVB.TestVbScope:Run'); }}";
+
+ await EvaluateAndCheck(
+ "window.setTimeout(function() {" + expression + "; }, 1);",
+ "dotnet://debugger-test-vb.dll/debugger-test-vb.vb", 14, 12,
+ "DebuggerTestVB.TestVbScope.RunVBScope",
+ locals_fn: async (locals) =>
+ {
+ await CheckString(locals, "testVbScope", "hello");
+ CheckNumber(locals, "a", 10);
+ CheckNumber(locals, "data", 10);
+ }
+ );
+ }
}
}
--- /dev/null
+Public Class TestVbScope
+ Public Shared Async Function Run() As Task
+ Await RunVBScope(10)
+ Await RunVBScope(1000)
+ End Function
+
+ Public Shared Async Function RunVBScope(data As Integer) As Task(Of Integer)
+ Dim a As Integer
+ a = 10
+ If data < 999 Then
+ Dim testVbScope As String
+ Dim onlyInFirstScope As String
+ testVbScope = "hello"
+ onlyInFirstScope = "only-in-first-scope"
+ System.Diagnostics.Debugger.Break()
+ Await Task.Delay(1)
+ Return data
+ Else
+ Dim testVbScope As String
+ Dim onlyInSecondScope As String
+ testVbScope = "hi"
+ onlyInSecondScope = "only-in-second-scope"
+ System.Diagnostics.Debugger.Break()
+ Await Task.Delay(1)
+ Return data
+ End If
+
+ End Function
+
+End Class
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <RootNamespace>DebuggerTestVB</RootNamespace>
+ <TargetFramework>net7.0</TargetFramework>
+ </PropertyGroup>
+
+</Project>
}
}
+ public class VariablesWithSameNameDifferentScopes
+ {
+ public static async Task Run()
+ {
+ await RunCSharpScope(10);
+ await RunCSharpScope(1000);
+ }
+
+ public static async Task<string> RunCSharpScope(int number)
+ {
+ await Task.Delay(1);
+ if (number < 999)
+ {
+ string testCSharpScope = "hello"; string onlyInFirstScope = "only-in-first-scope";
+ System.Diagnostics.Debugger.Break();
+ return testCSharpScope;
+ }
+ else
+ {
+ string testCSharpScope = "hi"; string onlyInSecondScope = "only-in-second-scope";
+ System.Diagnostics.Debugger.Break();
+ return testCSharpScope;
+ }
+ }
+
+ public static async Task RunContinueWith()
+ {
+ await RunContinueWithSameVariableName(10);
+ await RunContinueWithSameVariableName(1000);
+ }
+
+ public static async Task RunNestedContinueWith()
+ {
+ await RunNestedContinueWithSameVariableName(10);
+ await RunNestedContinueWithSameVariableName(1000);
+ }
+
+ public static async Task RunContinueWithSameVariableName(int number)
+ {
+ await Task.Delay(500).ContinueWith(async t =>
+ {
+ await Task.Delay(1);
+ if (number < 999)
+ {
+ var testCSharpScope = new String("hello"); string onlyInFirstScope = "only-in-first-scope";
+ System.Diagnostics.Debugger.Break();
+ return testCSharpScope;
+ }
+ else
+ {
+ var testCSharpScope = new String("hi"); string onlyInSecondScope = "only-in-second-scope";
+ System.Diagnostics.Debugger.Break();
+ return testCSharpScope;
+ }
+ });
+ Console.WriteLine ($"done with this method");
+ }
+
+ public static async Task RunNestedContinueWithSameVariableName(int number)
+ {
+ await Task.Delay(500).ContinueWith(async t =>
+ {
+ if (number < 999)
+ {
+ var testCSharpScope = new String("hello_out"); string onlyInFirstScope = "only-in-first-scope_out";
+ Console.WriteLine(testCSharpScope);
+ }
+ else
+ {
+ var testCSharpScope = new String("hi_out"); string onlyInSecondScope = "only-in-second-scope_out";
+ Console.WriteLine(testCSharpScope);
+ }
+ await Task.Delay(300).ContinueWith(t2 =>
+ {
+ if (number < 999)
+ {
+ var testCSharpScope = new String("hello"); string onlyInFirstScope = "only-in-first-scope";
+ System.Diagnostics.Debugger.Break();
+ return testCSharpScope;
+ }
+ else
+ {
+ var testCSharpScope = new String("hi"); string onlyInSecondScope = "only-in-second-scope";
+ System.Diagnostics.Debugger.Break();
+ return testCSharpScope;
+ }
+ });
+ });
+ Console.WriteLine ($"done with this method");
+ }
+
+ public static void RunNonAsyncMethod()
+ {
+ RunNonAsyncMethodSameVariableName(10);
+ RunNonAsyncMethodSameVariableName(1000);
+ }
+
+ public static string RunNonAsyncMethodSameVariableName(int number)
+ {
+ if (number < 999)
+ {
+ var testCSharpScope = new String("hello"); string onlyInFirstScope = "only-in-first-scope";
+ System.Diagnostics.Debugger.Break();
+ return testCSharpScope;
+ }
+ else
+ {
+ var testCSharpScope = new String("hi"); string onlyInSecondScope = "only-in-second-scope";
+ System.Diagnostics.Debugger.Break();
+ return testCSharpScope;
+ }
+ }
+ }
+
}
<ProjectReference Include="..\debugger-test-with-source-link\debugger-test-with-source-link.csproj" ReferenceOutputAssembly="false" Private="true" />
<ProjectReference Include="..\debugger-test-without-debug-symbols-to-load\debugger-test-without-debug-symbols-to-load.csproj" Private="true" />
<ProjectReference Include="..\debugger-test-with-non-user-code-class\debugger-test-with-non-user-code-class.csproj" Private="true" />
+ <ProjectReference Include="..\debugger-test-vb\debugger-test-vb.vbproj" Private="true" />
<!-- loaded by *tests*, and not the test app -->
<ProjectReference Include="..\lazy-debugger-test-embedded\lazy-debugger-test-embedded.csproj" ReferenceOutputAssembly="false" Private="true" />
<WasmAssembliesToBundle Include="$(OutDir)\debugger-test-with-source-link.dll" />
<WasmAssembliesToBundle Include="$(OutDir)\debugger-test-without-debug-symbols-to-load.dll" />
<WasmAssembliesToBundle Include="$(OutDir)\debugger-test-with-non-user-code-class.dll" />
+ <WasmAssembliesToBundle Include="$(OutDir)\debugger-test-vb.dll" />
<WasmAssembliesToBundle Include="$(MicrosoftNetCoreAppRuntimePackRidDir)\lib\$(NetCoreappCurrent)\System.Runtime.InteropServices.JavaScript.dll" />
<!-- Assemblies only dynamically loaded -->