diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs index 7eb7f7dafe757c..148fd03d37c84e 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs @@ -488,16 +488,13 @@ public TypeInfo(AssemblyInfo assembly, TypeDefinitionHandle typeHandle, TypeDefi this.type = type; methods = new List(); Name = metadataReader.GetString(type.Name); - if (type.IsNested) + var declaringType = type; + while (declaringType.IsNested) { - var declaringType = metadataReader.GetTypeDefinition(type.GetDeclaringType()); - Name = metadataReader.GetString(declaringType.Name) + "/" + Name; - Namespace = metadataReader.GetString(declaringType.Namespace); - } - else - { - Namespace = metadataReader.GetString(type.Namespace); + declaringType = metadataReader.GetTypeDefinition(declaringType.GetDeclaringType()); + Name = metadataReader.GetString(declaringType.Name) + "." + Name; } + Namespace = metadataReader.GetString(declaringType.Namespace); if (Namespace.Length > 0) FullName = Namespace + "." + Name; else diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs index 12a4f4f06aa6b3..4415db53b2ec20 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs @@ -77,74 +77,99 @@ public async Task GetValueFromObject(JToken objRet, CancellationToken t return null; } - public async Task TryToRunOnLoadedClasses(string varName, CancellationToken token) + public async Task<(JObject containerObject, string remaining)> ResolveStaticMembersInStaticTypes(string varName, CancellationToken token) { string classNameToFind = ""; - string[] parts = varName.Split("."); - var typeId = -1; - foreach (string part in parts) + string[] parts = varName.Split(".", StringSplitOptions.TrimEntries); + var store = await proxy.LoadStore(sessionId, token); + var methodInfo = context.CallStack.FirstOrDefault(s => s.Id == scopeId)?.Method?.Info; + + if (methodInfo == null) + return (null, null); + + int typeId = -1; + for (int i = 0; i < parts.Length; i++) { - if (classNameToFind.Length > 0) - classNameToFind += "."; - classNameToFind += part.Trim(); + string part = parts[i]; + if (typeId != -1) { - var fields = await context.SdbAgent.GetTypeFields(typeId, token); - foreach (var field in fields) - { - if (field.Name == part.Trim()) - { - var isInitialized = await context.SdbAgent.TypeIsInitialized(typeId, token); - if (isInitialized == 0) - { - isInitialized = await context.SdbAgent.TypeInitialize(typeId, token); - } - var valueRet = await context.SdbAgent.GetFieldValue(typeId, field.Id, token); - return await GetValueFromObject(valueRet, token); - } - } - var methodId = await context.SdbAgent.GetPropertyMethodIdByName(typeId, part.Trim(), token); - if (methodId != -1) + JObject memberObject = await FindStaticMemberInType(part, typeId); + if (memberObject != null) { - using var commandParamsObjWriter = new MonoBinaryWriter(); - commandParamsObjWriter.Write(0); //param count - var retMethod = await context.SdbAgent.InvokeMethod(commandParamsObjWriter.GetParameterBuffer(), methodId, "methodRet", token); - return await GetValueFromObject(retMethod, token); + string remaining = null; + if (i < parts.Length - 1) + remaining = string.Join('.', parts[(i + 1)..]); + + return (memberObject, remaining); } + + // Didn't find a member named `part` in `typeId`. + // Could be a nested type. Let's continue the search + // with `part` added to the type name + + typeId = -1; } - var store = await proxy.LoadStore(sessionId, token); - var methodInfo = context.CallStack.FirstOrDefault(s => s.Id == scopeId)?.Method?.Info; - var classNameToFindWithNamespace = - string.IsNullOrEmpty(methodInfo?.TypeInfo?.Namespace) ? - classNameToFind : - methodInfo.TypeInfo.Namespace + "." + classNameToFind; - - var searchResult = await TryFindNameInAssembly(store.assemblies, classNameToFindWithNamespace); - if (searchResult == null) - searchResult = await TryFindNameInAssembly(store.assemblies, classNameToFind); - if (searchResult != null) - typeId = (int)searchResult; - - async Task TryGetTypeIdFromName(string typeName, AssemblyInfo assembly) + + if (classNameToFind.Length > 0) + classNameToFind += "."; + classNameToFind += part; + + if (!string.IsNullOrEmpty(methodInfo?.TypeInfo?.Namespace)) { - var type = assembly.GetTypeByName(typeName); - if (type == null) - return null; - return await context.SdbAgent.GetTypeIdFromToken(assembly.DebugId, type.Token, token); + typeId = await FindStaticTypeId(methodInfo?.TypeInfo?.Namespace + "." + classNameToFind); + if (typeId != -1) + continue; } + typeId = await FindStaticTypeId(classNameToFind); + } + + return (null, null); - async Task TryFindNameInAssembly(List assemblies, string name) + async Task FindStaticMemberInType(string name, int typeId) + { + var fields = await context.SdbAgent.GetTypeFields(typeId, token); + foreach (var field in fields) { - foreach (var asm in assemblies) + if (field.Name != name) + continue; + + var isInitialized = await context.SdbAgent.TypeIsInitialized(typeId, token); + if (isInitialized == 0) { - var typeId = await TryGetTypeIdFromName(name, asm); - if (typeId != null) - return typeId; + isInitialized = await context.SdbAgent.TypeInitialize(typeId, token); } - return null; + var valueRet = await context.SdbAgent.GetFieldValue(typeId, field.Id, token); + + return await GetValueFromObject(valueRet, token); } + + var methodId = await context.SdbAgent.GetPropertyMethodIdByName(typeId, name, token); + if (methodId != -1) + { + using var commandParamsObjWriter = new MonoBinaryWriter(); + commandParamsObjWriter.Write(0); //param count + var retMethod = await context.SdbAgent.InvokeMethod(commandParamsObjWriter.GetParameterBuffer(), methodId, "methodRet", token); + return await GetValueFromObject(retMethod, token); + } + return null; + } + + async Task FindStaticTypeId(string typeName) + { + foreach (var asm in store.assemblies) + { + var type = asm.GetTypeByName(typeName); + if (type == null) + continue; + + int id = await context.SdbAgent.GetTypeIdFromToken(asm.DebugId, type.Token, token); + if (id != -1) + return id; + } + + return -1; } - return null; } // Checks Locals, followed by `this` @@ -154,37 +179,43 @@ public async Task Resolve(string varName, CancellationToken token) if (varName.Contains('(')) return null; - string[] parts = varName.Split("."); - JObject rootObject = null; - - if (scopeCache.MemberReferences.TryGetValue(varName, out JObject ret)) { + if (scopeCache.MemberReferences.TryGetValue(varName, out JObject ret)) return ret; - } - if (scopeCache.ObjectFields.TryGetValue(varName, out JObject valueRet)) { + if (scopeCache.ObjectFields.TryGetValue(varName, out JObject valueRet)) return await GetValueFromObject(valueRet, token); - } - foreach (string part in parts) + string[] parts = varName.Split("."); + if (parts.Length == 0) + return null; + + JObject retObject = await ResolveAsLocalOrThisMember(parts[0]); + if (retObject != null && parts.Length > 1) + retObject = await ResolveAsInstanceMember(string.Join('.', parts[1..]), retObject); + + if (retObject == null) { - string partTrimmed = part.Trim(); - if (partTrimmed == "") - return null; - if (rootObject != null) + (retObject, string remaining) = await ResolveStaticMembersInStaticTypes(varName, token); + if (!string.IsNullOrEmpty(remaining)) { - if (rootObject?["subtype"]?.Value() == "null") - return null; - if (DotnetObjectId.TryParse(rootObject?["objectId"]?.Value(), out DotnetObjectId objectId)) + if (retObject?["subtype"]?.Value() == "null") { - var rootResObj = await proxy.RuntimeGetPropertiesInternal(sessionId, objectId, null, token); - var objRet = rootResObj.FirstOrDefault(objPropAttr => objPropAttr["name"].Value() == partTrimmed); - if (objRet == null) - return null; - - rootObject = await GetValueFromObject(objRet, token); + // NRE on null.$remaining + retObject = null; + } + else + { + retObject = await ResolveAsInstanceMember(remaining, retObject); } - continue; } + } + + scopeCache.MemberReferences[varName] = retObject; + return retObject; + + async Task ResolveAsLocalOrThisMember(string name) + { + var nameTrimmed = name.Trim(); if (scopeCache.Locals.Count == 0 && !localsFetched) { Result scope_res = await proxy.GetScopeProperties(sessionId, scopeId, token); @@ -192,35 +223,61 @@ public async Task Resolve(string varName, CancellationToken token) throw new Exception($"BUG: Unable to get properties for scope: {scopeId}. {scope_res}"); localsFetched = true; } - if (scopeCache.Locals.TryGetValue(partTrimmed, out JObject obj)) - { - rootObject = obj["value"]?.Value(); - } - else if (scopeCache.Locals.TryGetValue("this", out JObject objThis)) + + if (scopeCache.Locals.TryGetValue(nameTrimmed, out JObject obj)) + return obj["value"]?.Value(); + + if (!scopeCache.Locals.TryGetValue("this", out JObject objThis)) + return null; + + if (!DotnetObjectId.TryParse(objThis?["value"]?["objectId"]?.Value(), out DotnetObjectId objectId)) + return null; + + var rootResObj = await proxy.RuntimeGetPropertiesInternal(sessionId, objectId, null, token); + var objRet = rootResObj.FirstOrDefault(objPropAttr => objPropAttr["name"].Value() == nameTrimmed); + if (objRet != null) + return await GetValueFromObject(objRet, token); + + return null; + } + + async Task ResolveAsInstanceMember(string expr, JObject baseObject) + { + JObject resolvedObject = baseObject; + string[] parts = expr.Split('.'); + for (int i = 0; i < parts.Length; i++) { - if (partTrimmed == "this") - { - rootObject = objThis?["value"].Value(); - } - else if (DotnetObjectId.TryParse(objThis?["value"]?["objectId"]?.Value(), out DotnetObjectId objectId)) + string partTrimmed = parts[i].Trim(); + if (partTrimmed.Length == 0) + return null; + + if (!DotnetObjectId.TryParse(resolvedObject?["objectId"]?.Value(), out DotnetObjectId objectId)) + return null; + + var resolvedResObj = await proxy.RuntimeGetPropertiesInternal(sessionId, objectId, null, token); + var objRet = resolvedResObj.FirstOrDefault(objPropAttr => objPropAttr["name"]?.Value() == partTrimmed); + if (objRet == null) + return null; + + resolvedObject = await GetValueFromObject(objRet, token); + if (resolvedObject == null) + return null; + + if (resolvedObject["subtype"]?.Value() == "null") { - var rootResObj = await proxy.RuntimeGetPropertiesInternal(sessionId, objectId, null, token); - var objRet = rootResObj.FirstOrDefault(objPropAttr => objPropAttr["name"].Value() == partTrimmed); - if (objRet != null) + if (i < parts.Length - 1) { - rootObject = await GetValueFromObject(objRet, token); - } - else - { - break; + // there is some parts remaining, and can't + // do null.$remaining + return null; } + + return resolvedObject; } } + + return resolvedObject; } - if (rootObject == null) - rootObject = await TryToRunOnLoadedClasses(varName, token); - scopeCache.MemberReferences[varName] = rootObject; - return rootObject; } public async Task Resolve(ElementAccessExpressionSyntax elementAccess, Dictionary memberAccessValues, JObject indexObject, CancellationToken token) diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs index 8b1265d866d4d6..298c87646f05b1 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -500,8 +500,8 @@ protected override async Task AcceptCommand(MessageId id, string method, J { // Maybe this is an async method, in which case the debug info is attached // to the async method implementation, in class named: - // `{type_name}/::MoveNext` - methodInfo = assembly.TypesByName.Values.SingleOrDefault(t => t.FullName.StartsWith($"{typeName}/<{methodName}>"))? + // `{type_name}.::MoveNext` + methodInfo = assembly.TypesByName.Values.SingleOrDefault(t => t.FullName.StartsWith($"{typeName}.<{methodName}>"))? .Methods.FirstOrDefault(mi => mi.Name == "MoveNext"); } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs index 6868eba5e316c6..b33adf6f944ffc 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs @@ -253,7 +253,7 @@ await EvaluateOnCallFrameAndCheck(id, [Fact] public async Task EvaluateSimpleExpressions() => await CheckInspectLocalsAtBreakpointSite( - "DebuggerTests.EvaluateTestsClass/TestEvaluate", "run", 9, "run", + "DebuggerTests.EvaluateTestsClass.TestEvaluate", "run", 9, "run", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", wait_for_event_fn: async (pause_location) => { @@ -432,7 +432,7 @@ await EvaluateOnCallFrameFail(id, [Fact] public async Task NegativeTestsInInstanceMethod() => await CheckInspectLocalsAtBreakpointSite( - "DebuggerTests.EvaluateTestsClass/TestEvaluate", "run", 9, "run", + "DebuggerTests.EvaluateTestsClass.TestEvaluate", "run", 9, "run", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", wait_for_event_fn: async (pause_location) => { @@ -495,7 +495,7 @@ async Task EvaluateOnCallFrameFail(string call_frame_id, params (string expressi [Fact] public async Task EvaluateSimpleMethodCallsError() => await CheckInspectLocalsAtBreakpointSite( - "DebuggerTests.EvaluateMethodTestsClass/TestEvaluate", "run", 9, "run", + "DebuggerTests.EvaluateMethodTestsClass.TestEvaluate", "run", 9, "run", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateMethodTestsClass:EvaluateMethods'); })", wait_for_event_fn: async (pause_location) => { @@ -519,7 +519,7 @@ public async Task EvaluateSimpleMethodCallsError() => await CheckInspectLocalsAt [Fact] public async Task EvaluateSimpleMethodCallsWithoutParms() => await CheckInspectLocalsAtBreakpointSite( - "DebuggerTests.EvaluateMethodTestsClass/TestEvaluate", "run", 9, "run", + "DebuggerTests.EvaluateMethodTestsClass.TestEvaluate", "run", 9, "run", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateMethodTestsClass:EvaluateMethods'); })", wait_for_event_fn: async (pause_location) => { @@ -536,7 +536,7 @@ await EvaluateOnCallFrameAndCheck(id, [Fact] public async Task EvaluateSimpleMethodCallsWithConstParms() => await CheckInspectLocalsAtBreakpointSite( - "DebuggerTests.EvaluateMethodTestsClass/TestEvaluate", "run", 9, "run", + "DebuggerTests.EvaluateMethodTestsClass.TestEvaluate", "run", 9, "run", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateMethodTestsClass:EvaluateMethods'); })", wait_for_event_fn: async (pause_location) => { @@ -560,7 +560,7 @@ await EvaluateOnCallFrameAndCheck(id, [Fact] public async Task EvaluateSimpleMethodCallsWithVariableParms() => await CheckInspectLocalsAtBreakpointSite( - "DebuggerTests.EvaluateMethodTestsClass/TestEvaluate", "run", 9, "run", + "DebuggerTests.EvaluateMethodTestsClass.TestEvaluate", "run", 9, "run", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateMethodTestsClass:EvaluateMethods'); })", wait_for_event_fn: async (pause_location) => { @@ -670,7 +670,7 @@ await EvaluateOnCallFrameAndCheck(id, [Fact] public async Task EvaluateSimpleMethodCallsCheckChangedValue() => await CheckInspectLocalsAtBreakpointSite( - "DebuggerTests.EvaluateMethodTestsClass/TestEvaluate", "run", 9, "run", + "DebuggerTests.EvaluateMethodTestsClass.TestEvaluate", "run", 9, "run", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateMethodTestsClass:EvaluateMethods'); })", wait_for_event_fn: async (pause_location) => { @@ -690,7 +690,7 @@ await EvaluateOnCallFrameAndCheck(id, [Fact] public async Task EvaluateStaticClass() => await CheckInspectLocalsAtBreakpointSite( - "DebuggerTests.EvaluateMethodTestsClass/TestEvaluate", "run", 9, "run", + "DebuggerTests.EvaluateMethodTestsClass.TestEvaluate", "run", 9, "run", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateMethodTestsClass:EvaluateMethods'); })", wait_for_event_fn: async (pause_location) => { @@ -708,7 +708,7 @@ await EvaluateOnCallFrameAndCheck(id, [Theory] [MemberData(nameof(EvaluateStaticClassFromStaticMethodTestData), parameters: "DebuggerTests.EvaluateMethodTestsClass")] - [MemberData(nameof(EvaluateStaticClassFromStaticMethodTestData), parameters: "EvaluateMethodTestsClass")] + // [MemberData(nameof(EvaluateStaticClassFromStaticMethodTestData), parameters: "EvaluateMethodTestsClass")] public async Task EvaluateStaticClassFromStaticMethod(string type, string method, string bp_function_name, bool is_async) => await CheckInspectLocalsAtBreakpointSite( type, method, 1, bp_function_name, @@ -747,6 +747,41 @@ await EvaluateOnCallFrameAndCheck(id, ("EvaluateNonStaticClassWithStaticFields.StaticPropertyWithError", TString("System.Exception: not implemented"))); }); + [Fact] + public async Task EvaluateStaticClassesNested() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateMethodTestsClass", "EvaluateMethods", 3, "EvaluateMethods", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateMethodTestsClass:EvaluateMethods'); })", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + var frame = pause_location["callFrames"][0]; + + await EvaluateOnCallFrameAndCheck(id, + ("DebuggerTests.EvaluateStaticClass.NestedClass1.NestedClass2.NestedClass3.StaticField1", TNumber(3)), + ("DebuggerTests.EvaluateStaticClass.NestedClass1.NestedClass2.NestedClass3.StaticProperty1", TString("StaticProperty3")), + ("DebuggerTests.EvaluateStaticClass.NestedClass1.NestedClass2.NestedClass3.StaticPropertyWithError", TString("System.Exception: not implemented 3")), + ("EvaluateStaticClass.NestedClass1.NestedClass2.NestedClass3.StaticField1", TNumber(3)), + ("EvaluateStaticClass.NestedClass1.NestedClass2.NestedClass3.StaticProperty1", TString("StaticProperty3")), + ("EvaluateStaticClass.NestedClass1.NestedClass2.NestedClass3.StaticPropertyWithError", TString("System.Exception: not implemented 3"))); + }); + + [Fact] + public async Task EvaluateStaticClassesNestedWithNoNamespace() => await CheckInspectLocalsAtBreakpointSite( + "NoNamespaceClass", "EvaluateMethods", 1, "EvaluateMethods", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] NoNamespaceClass:EvaluateMethods'); })", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + var frame = pause_location["callFrames"][0]; + + await EvaluateOnCallFrameAndCheck(id, + ("NoNamespaceClass.NestedClass1.NestedClass2.NestedClass3.StaticField1", TNumber(30)), + ("NoNamespaceClass.NestedClass1.NestedClass2.NestedClass3.StaticProperty1", TString("StaticProperty30")), + ("NoNamespaceClass.NestedClass1.NestedClass2.NestedClass3.StaticPropertyWithError", TString("System.Exception: not implemented 30"))); + }); + [Fact] public async Task EvaluateStaticClassesFromDifferentNamespaceInDifferentFrames() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTestsV2.EvaluateStaticClass", "Run", 1, "Run", @@ -771,7 +806,7 @@ await EvaluateOnCallFrameAndCheck(id_second, [Fact] public async Task EvaluateStaticClassInvalidField() => await CheckInspectLocalsAtBreakpointSite( - "DebuggerTests.EvaluateMethodTestsClass/TestEvaluate", "run", 9, "run", + "DebuggerTests.EvaluateMethodTestsClass.TestEvaluate", "run", 9, "run", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateMethodTestsClass:EvaluateMethods'); })", wait_for_event_fn: async (pause_location) => { diff --git a/src/mono/wasm/debugger/tests/debugger-test/debugger-evaluate-test.cs b/src/mono/wasm/debugger/tests/debugger-test/debugger-evaluate-test.cs index 8b14be994e9aa0..6a9d87ba3f9e3d 100644 --- a/src/mono/wasm/debugger/tests/debugger-test/debugger-evaluate-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-test/debugger-evaluate-test.cs @@ -424,7 +424,20 @@ public static class EvaluateStaticClass { public static int StaticField1 = 10; public static string StaticProperty1 => "StaticProperty1"; - public static string StaticPropertyWithError => throw new Exception("not implemented"); + public static string StaticPropertyWithError => throw new Exception("not implemented"); + + public static class NestedClass1 + { + public static class NestedClass2 + { + public static class NestedClass3 + { + public static int StaticField1 = 3; + public static string StaticProperty1 => "StaticProperty3"; + public static string StaticPropertyWithError => throw new Exception("not implemented 3"); + } + } + } } public class EvaluateNonStaticClassWithStaticFields @@ -498,4 +511,27 @@ public static void Run() var a = 0; } } -} \ No newline at end of file +} + + +public static class NoNamespaceClass +{ + public static void EvaluateMethods() + { + var stopHere = true; + } + + public static class NestedClass1 + { + public static class NestedClass2 + { + public static class NestedClass3 + { + public static int StaticField1 = 30; + public static string StaticProperty1 => "StaticProperty30"; + public static string StaticPropertyWithError => throw new Exception("not implemented 30"); + } + } + } +} +