diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs index ff76100ea874..960157f8176c 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs @@ -473,6 +473,7 @@ internal sealed class PerScopeCache public Dictionary Locals { get; } = new Dictionary(); public Dictionary MemberReferences { get; } = new Dictionary(); public Dictionary ObjectFields { get; } = new Dictionary(); + public Dictionary EvaluationResults { get; } = new(); public PerScopeCache(JArray objectValues) { foreach (var objectValue in objectValues) diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs index 7dc35ac24e07..159cb3ec5e27 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs @@ -453,7 +453,7 @@ private static async Task> ResolveElementAccess(IEnumerable FindMethodIdOnLinqEnumerable(IList typeIds, string methodNa return 0; } } + + private static readonly HashSet NumericTypes = new HashSet + { + typeof(decimal), typeof(byte), typeof(sbyte), + typeof(short), typeof(ushort), + typeof(int), typeof(uint), + typeof(float), typeof(double) + }; + + public object ConvertCSharpToJSType(object v, Type type) + { + if (v == null) + return new { type = "object", subtype = "null", className = type?.ToString(), description = type?.ToString() }; + if (v is string s) + return new { type = "string", value = s, description = s }; + if (v is char c) + return new { type = "symbol", value = c, description = $"{(int)c} '{c}'" }; + if (NumericTypes.Contains(v.GetType())) + return new { type = "number", value = v, description = Convert.ToDouble(v).ToString(CultureInfo.InvariantCulture) }; + if (v is bool) + return new { type = "boolean", value = v, description = v.ToString().ToLowerInvariant(), className = type.ToString() }; + if (v is JObject) + return v; + if (v is Array arr) + { + return CacheEvaluationResult( + JObject.FromObject( + new + { + type = "object", + subtype = "array", + value = new JArray(arr.Cast().Select((val, idx) => JObject.FromObject( + new + { + value = ConvertCSharpToJSType(val, val.GetType()), + name = $"{idx}" + }))), + description = v.ToString(), + className = type.ToString() + })); + } + return new { type = "object", value = v, description = v.ToString(), className = type.ToString() }; + } + + private JObject CacheEvaluationResult(JObject value) + { + if (IsDuplicated(value, out JObject duplicate)) + return value; + + var evalResultId = Interlocked.Increment(ref evaluationResultObjectId); + string id = $"dotnet:evaluationResult:{evalResultId}"; + if (!value.TryAdd("objectId", id)) + { + logger.LogWarning($"EvaluationResult cache request passed with ID: {value["objectId"].Value()}. Overwritting it with a automatically assigned ID: {id}."); + value["objectId"] = id; + } + scopeCache.EvaluationResults.Add(id, value); + return value; + + bool IsDuplicated(JObject er, out JObject duplicate) + { + var type = er["type"].Value(); + var subtype = er["subtype"].Value(); + var value = er["value"]; + var description = er["description"].Value(); + var className = er["className"].Value(); + duplicate = scopeCache.EvaluationResults.FirstOrDefault( + pair => pair.Value["type"].Value() == type + && pair.Value["subtype"].Value() == subtype + && pair.Value["description"].Value() == description + && pair.Value["className"].Value() == className + && JToken.DeepEquals(pair.Value["value"], value)).Value; + return duplicate != null; + } + } + + public JObject TryGetEvaluationResult(string id) + { + JObject val; + if (!scopeCache.EvaluationResults.TryGetValue(id, out val)) + logger.LogError($"EvaluationResult of ID: {id} does not exist in the cache."); + return val; + } } } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs index c7ba8454716e..f93b61af9ef7 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -802,6 +802,9 @@ internal async Task> RuntimeGetObjectMembers(Sess return value_json_str != null ? ValueOrError.WithValue(GetMembersResult.FromValues(JArray.Parse(value_json_str))) : ValueOrError.WithError(res); + case "evaluationResult": + JArray evaluationRes = (JArray)context.SdbAgent.GetEvaluationResultProperties(objectId.ToString()); + return ValueOrError.WithValue(GetMembersResult.FromValues(evaluationRes)); default: return ValueOrError.WithError($"RuntimeGetProperties: unknown object id scheme: {objectId.Scheme}"); } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs index 663c022ff622..a95715a4ef19 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs @@ -1387,6 +1387,14 @@ public async Task GetAssemblyFromType(int type_id, CancellationToken token) return retDebuggerCmdReader.ReadInt32(); } + public JToken GetEvaluationResultProperties(string id) + { + ExecutionContext context = proxy.GetContext(sessionId); + var resolver = new MemberReferenceResolver(proxy, context, sessionId, context.CallStack.First().Id, logger); + var evaluationResult = resolver.TryGetEvaluationResult(id); + return evaluationResult["value"]; + } + public async Task GetValueFromDebuggerDisplayAttribute(DotnetObjectId dotnetObjectId, int typeId, CancellationToken token) { string expr = ""; diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestFirefox.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestFirefox.cs index e5712b232e19..c43a1217e0e4 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestFirefox.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestFirefox.cs @@ -313,7 +313,7 @@ internal override async Task GetProperties(string id, JToken fn_args = n } return ret; } - if (id.StartsWith("dotnet:valuetype:") || id.StartsWith("dotnet:object:") || id.StartsWith("dotnet:array:") || id.StartsWith("dotnet:pointer:")) + if (id.StartsWith("dotnet:evaluationResult:") || id.StartsWith("dotnet:valuetype:") || id.StartsWith("dotnet:object:") || id.StartsWith("dotnet:array:") || id.StartsWith("dotnet:pointer:")) { JArray ret = new (); var o = JObject.FromObject(new diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs index 0b5fed89ad48..76cc33929ad6 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs @@ -1201,5 +1201,24 @@ public async Task EvaluateLocalObjectFromAssemblyNotRelatedButLoaded() ("localFloat.ToString()", TString(floatLocalVal["description"]?.Value())), ("localDouble.ToString()", TString(doubleLocalVal["description"]?.Value()))); }); + + [Fact] + public async Task EvaluateMethodsOnPrimitiveTypesReturningObjects() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.PrimitiveTypeMethods", "Evaluate", 11, "Evaluate", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.PrimitiveTypeMethods:Evaluate'); })", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + var (res, _) = await EvaluateOnCallFrame(id, "test.propString.Split('_', 3, System.StringSplitOptions.TrimEntries)"); + var props = res["value"] ?? await GetProperties(res["objectId"]?.Value()); // in firefox getProps is necessary + var expected_props = new [] { TString("s"), TString("t"), TString("r") }; + await CheckProps(props, expected_props, "props#1"); + + (res, _) = await EvaluateOnCallFrame(id, "localString.Split('*', 3, System.StringSplitOptions.RemoveEmptyEntries)"); + props = res["value"] ?? await GetProperties(res["objectId"]?.Value()); + expected_props = new [] { TString("S"), TString("T"), TString("R") }; + await CheckProps(props, expected_props, "props#2"); + }); } }