diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs index beb2903c360d8..47d0f2a627c63 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.IO; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -82,68 +81,86 @@ 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) + var store = await proxy.LoadStore(sessionId, token); + var methodInfo = ctx.CallStack.FirstOrDefault(s => s.Id == scopeId).Method.Info; + + int typeId = -1; + for (int i = 0; i < parts.Length; i++) { + string part = parts[i].Trim(); + if (classNameToFind.Length > 0) classNameToFind += "."; - classNameToFind += part.Trim(); + classNameToFind += part; + if (typeId != -1) { - var fields = await sdbHelper.GetTypeFields(sessionId, typeId, token); - foreach (var field in fields) - { - if (field.Name == part.Trim()) - { - var isInitialized = await sdbHelper.TypeIsInitialized(sessionId, typeId, token); - if (isInitialized == 0) - { - isInitialized = await sdbHelper.TypeInitialize(sessionId, typeId, token); - } - var valueRet = await sdbHelper.GetFieldValue(sessionId, typeId, field.Id, token); - return await GetValueFromObject(valueRet, token); - } - } - var methodId = await sdbHelper.GetPropertyMethodIdByName(sessionId, typeId, part.Trim(), token); - if (methodId != -1) + string remaining = null; + JObject memberObject = await FindStaticMemberInType(part, typeId); + if (memberObject != null && i < parts.Length - 1) + remaining = string.Join('.', parts[(i+1)..]); + + return (memberObject, remaining); + } + + if (!string.IsNullOrEmpty(methodInfo.TypeInfo.Namespace)) + typeId = await FindStaticTypeId(methodInfo.TypeInfo.Namespace + "." + classNameToFind); + if (typeId == -1) + typeId = await FindStaticTypeId(classNameToFind); + } + + return (null, null); + + async Task FindStaticMemberInType(string name, int typeId) + { + var fields = await sdbHelper.GetTypeFields(sessionId, typeId, token); + foreach (var field in fields) + { + if (field.Name != name) + continue; + + var isInitialized = await sdbHelper.TypeIsInitialized(sessionId, typeId, token); + if (isInitialized == 0) { - var commandParamsObj = new MemoryStream(); - var commandParamsObjWriter = new MonoBinaryWriter(commandParamsObj); - commandParamsObjWriter.Write(0); //param count - var retMethod = await sdbHelper.InvokeMethod(sessionId, commandParamsObj.ToArray(), methodId, "methodRet", token); - return await GetValueFromObject(retMethod, token); + isInitialized = await sdbHelper.TypeInitialize(sessionId, typeId, token); } + var valueRet = await sdbHelper.GetFieldValue(sessionId, typeId, field.Id, token); + + return await GetValueFromObject(valueRet, token); } - var store = await proxy.LoadStore(sessionId, token); - var info = ctx.CallStack.FirstOrDefault(s => s.Id == scopeId).Method.Info; - var classNameToFindWithNamespace = - string.IsNullOrEmpty(info.TypeInfo.Namespace) ? - classNameToFind : - info.TypeInfo.Namespace + "." + classNameToFind; - foreach (var asm in store.assemblies) + var methodId = await sdbHelper.GetPropertyMethodIdByName(sessionId, typeId, name, token); + if (methodId != -1) { - if (await TryGetTypeIdFromName(classNameToFindWithNamespace, asm)) - break; - if (await TryGetTypeIdFromName(classNameToFind, asm)) - break; + var commandParamsObj = new MemoryStream(); + var commandParamsObjWriter = new MonoBinaryWriter(commandParamsObj); + commandParamsObjWriter.Write(0); //param count + var retMethod = await sdbHelper.InvokeMethod(sessionId, commandParamsObj.ToArray(), methodId, "methodRet", token); + return await GetValueFromObject(retMethod, token); } - async Task TryGetTypeIdFromName(string typeName, AssemblyInfo assembly) + return null; + } + + async Task FindStaticTypeId(string typeName) + { + foreach (var asm in store.assemblies) { - var type = assembly.GetTypeByName(typeName); + var type = asm.GetTypeByName(typeName); if (type == null) - return false; + continue; - typeId = await sdbHelper.GetTypeIdFromToken(sessionId, assembly.DebugId, type.Token, token); - return true; + int id = await sdbHelper.GetTypeIdFromToken(sessionId, asm.DebugId, type.Token, token); + if (id != -1) + return id; } + + return -1; } - return null; } // Checks Locals, followed by `this` @@ -153,9 +170,6 @@ 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)) { return ret; } @@ -164,26 +178,36 @@ public async Task Resolve(string varName, CancellationToken token) 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) + { if (scopeCache.Locals.Count == 0 && !localsFetched) { Result scope_res = await proxy.GetScopeProperties(sessionId, scopeId, token); @@ -191,39 +215,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(name, 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() == name); + 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 - { - rootObject = await TryToRunOnLoadedClasses(varName, token); - return rootObject; + // 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); - return rootObject; - } - 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/DebuggerTestSuite/EvaluateOnCallFrameTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs index e7da3d4bfa59c..5579e50112762 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs @@ -442,6 +442,10 @@ await EvaluateOnCallFrameFail(id, //BUG: TODO: //("a)", "CompilationError"), + ("this.c.e", "ReferenceError"), // this.. + ("this.e.a", "ReferenceError"), // this.. + ("this.dt.e", "ReferenceError"), // this.. + ("this.a.", "ReferenceError"), ("a.", "ReferenceError"), @@ -465,6 +469,9 @@ public async Task NegativeTestsInStaticMethod() => await CheckInspectLocalsAtBre var id = pause_location["callFrames"][0]["callFrameId"].Value(); await EvaluateOnCallFrameFail(id, + ("EvaluateStaticClass.f_s.c", "ReferenceError"), // .. + ("EvaluateStaticClass.NonExistant.f_s.c", "ReferenceError"), // ... + ("NonExistant.f_s.dateTime", "ReferenceError"), // .... ("me.foo", "ReferenceError"), ("this", "ReferenceError"), ("this.NullIfAIsNotZero.foo", "ReferenceError")); @@ -693,10 +700,11 @@ public async Task EvaluateStaticClass() => await CheckInspectLocalsAtBreakpointS var frame = pause_location["callFrames"][0]; await EvaluateOnCallFrameAndCheck(id, - ("DebuggerTests.EvaluateStaticClass.StaticField1", TNumber(10))); - await EvaluateOnCallFrameAndCheck(id, - ("DebuggerTests.EvaluateStaticClass.StaticProperty1", TString("StaticProperty1"))); - await EvaluateOnCallFrameAndCheck(id, + ("EvaluateStaticClass.StaticDTProperty1.Hour", TNumber(4)), // .. + ("EvaluateStaticClass.StaticDTField1.Hour", TNumber(3)), // .. + ("EvaluateStaticClass.StaticDTField1.Date.Year", TNumber(2000)), // ... + ("DebuggerTests.EvaluateStaticClass.StaticField1", TNumber(10)), + ("DebuggerTests.EvaluateStaticClass.StaticProperty1", TString("StaticProperty1")), ("DebuggerTests.EvaluateStaticClass.StaticPropertyWithError", TString("System.Exception: not implemented"))); }); @@ -769,16 +777,16 @@ public async Task EvaluateStaticClassesFromDifferentNamespaceInDifferentFrames() var frame = pause_location["callFrames"][0]; await EvaluateOnCallFrameAndCheck(id_top, - ("EvaluateStaticClass.StaticField1", TNumber(20)), - ("EvaluateStaticClass.StaticProperty1", TString("StaticProperty2")), - ("EvaluateStaticClass.StaticPropertyWithError", TString("System.Exception: not implemented"))); + ("EvaluateStaticClass.StaticField1", TNumber(20)), + ("EvaluateStaticClass.StaticProperty1", TString("StaticProperty2")), + ("EvaluateStaticClass.StaticPropertyWithError", TString("System.Exception: not implemented"))); var id_second = pause_location["callFrames"][1]["callFrameId"].Value(); await EvaluateOnCallFrameAndCheck(id_second, - ("EvaluateStaticClass.StaticField1", TNumber(10)), - ("EvaluateStaticClass.StaticProperty1", TString("StaticProperty1")), - ("EvaluateStaticClass.StaticPropertyWithError", TString("System.Exception: not implemented"))); + ("EvaluateStaticClass.StaticField1", TNumber(10)), + ("EvaluateStaticClass.StaticProperty1", TString("StaticProperty1")), + ("EvaluateStaticClass.StaticPropertyWithError", TString("System.Exception: not implemented"))); }); [Fact] 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 0d8cd9ada9046..86c23a320edb2 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 @@ -418,7 +418,9 @@ public static void EvaluateAsyncMethods() public static class EvaluateStaticClass { public static int StaticField1 = 10; + public static DateTime StaticDTField1 = new DateTime(2000, 5, 4, 3, 2, 1); public static string StaticProperty1 => "StaticProperty1"; + public static DateTime StaticDTProperty1 => new DateTime(2000, 8, 1, 4, 7, 1); public static string StaticPropertyWithError => throw new Exception("not implemented"); } @@ -466,7 +468,7 @@ public void run() textListOfLists = new List> { textList, textList }; idx0 = 0; idx1 = 1; - } + } } public static void EvaluateLocals() @@ -493,4 +495,4 @@ public static void Run() var a = 0; } } -} \ No newline at end of file +}