diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs index 455517f8734331..c384f74e9c7223 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs @@ -24,6 +24,11 @@ namespace Microsoft.WebAssembly.Diagnostics { + internal sealed record VariableDefinition( + string IdName, + JObject Obj, + string Definition); + internal static partial class ExpressionEvaluator { internal static Script script = CSharpScript.Create( @@ -47,7 +52,8 @@ private sealed partial class ExpressionSyntaxReplacer : CSharpSyntaxWalker private int visitCount; public bool hasMethodCalls; public bool hasElementAccesses; - internal List variableDefinitions = new List(); + public bool hasStringExpressionStatement; + internal List variableDefinitions = new (); public void VisitInternal(SyntaxNode node) { @@ -92,6 +98,13 @@ public override void Visit(SyntaxNode node) hasElementAccesses = true; } + if (node is BinaryExpressionSyntax) + { + var binaryExpression = node as BinaryExpressionSyntax; + if (binaryExpression.Left.Kind() == SyntaxKind.StringLiteralExpression || binaryExpression.Right.Kind() == SyntaxKind.StringLiteralExpression) + hasStringExpressionStatement = true; + } + if (node is AssignmentExpressionSyntax) throw new Exception("Assignment is not implemented yet"); base.Visit(node); @@ -214,7 +227,7 @@ void AddLocalVariableWithValue(string idName, JObject value) if (localsSet.Contains(idName)) return; localsSet.Add(idName); - variableDefinitions.Add(ConvertJSToCSharpLocalVariableAssignment(idName, value)); + variableDefinitions.Add(new (idName, value, ConvertJSToCSharpLocalVariableAssignment(idName, value))); } } } @@ -445,12 +458,49 @@ internal static async Task CompileAndRunTheExpression( syntaxTree = replacer.ReplaceVars(syntaxTree, null, null, null, elementAccessValues); } - expressionTree = syntaxTree.GetCompilationUnitRoot(token); if (expressionTree == null) throw new Exception($"BUG: Unable to evaluate {expression}, could not get expression from the syntax tree"); + var variableDef = await GetVariableDefinitions(resolver, replacer.variableDefinitions, invokeToStringInObject: replacer.hasStringExpressionStatement, token); + return await EvaluateSimpleExpression(resolver, syntaxTree.ToString(), expression, variableDef, logger, token); + } + + internal static async Task> GetVariableDefinitions(MemberReferenceResolver resolver, List variableDefinitions, bool invokeToStringInObject, CancellationToken token) + { + var variableDefStrings = new List(); + foreach (var definition in variableDefinitions) + { + if (!invokeToStringInObject || definition.Obj?["type"]?.Value() != "object") + { + variableDefStrings.Add(definition.Definition); + continue; + } + + if (definition.Obj["subtype"]?.Value()?.Equals("null") == true) + { + variableDefStrings.Add($"string {definition.IdName} = \"\";"); + continue; + } - return await EvaluateSimpleExpression(resolver, syntaxTree.ToString(), expression, replacer.variableDefinitions, logger, token); + if (DotnetObjectId.TryParse(definition.Obj?["objectId"]?.Value(), out DotnetObjectId objectId)) + { + if (objectId.IsValueType) + { + variableDefStrings.Add($"string {definition.IdName} = \"{definition.Obj["description"].Value()}\";"); + } + else + { + var typeIds = await resolver.GetContext().SdbAgent.GetTypeIdsForObject(objectId.Value, withParents: true, token); + var toString = await resolver.GetContext().SdbAgent.InvokeToStringAsync(typeIds, isValueType: false, isEnum: false, objectId.Value, BindingFlags.DeclaredOnly, invokeToStringInObject: true, token); + variableDefStrings.Add($"string {definition.IdName} = \"{toString}\";"); + } + } + else + { + variableDefStrings.Add(definition.Definition); + } + } + return variableDefStrings; } internal static async Task EvaluateSimpleExpression( diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/JObjectValueCreator.cs b/src/mono/wasm/debugger/BrowserDebugProxy/JObjectValueCreator.cs index 3df1c9ba22b140..6fe370a0da62af 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/JObjectValueCreator.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/JObjectValueCreator.cs @@ -297,7 +297,7 @@ private async Task ReadAsObjectValue(MonoBinaryReader retDebuggerCmdRea } else { - var toString = await _sdbAgent.InvokeToStringAsync(typeIds, isValueType: false, isEnum: false, objectId, BindingFlags.DeclaredOnly, token); + var toString = await _sdbAgent.InvokeToStringAsync(typeIds, isValueType: false, isEnum: false, objectId, BindingFlags.DeclaredOnly, invokeToStringInObject: false, token); if (toString != null) description = toString; } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs index 73bc4640bcd92d..480f5dbc54212c 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs @@ -31,6 +31,7 @@ internal sealed class MemberReferenceResolver private readonly ILogger logger; private bool localsFetched; private int linqTypeId; + public ExecutionContext GetContext() => context; public MemberReferenceResolver(MonoProxy proxy, ExecutionContext ctx, SessionId sessionId, int scopeId, ILogger logger) { @@ -365,7 +366,7 @@ async Task ResolveAsInstanceMember(ArraySegment parts, JObject } } - public async Task Resolve(ElementAccessExpressionSyntax elementAccess, Dictionary memberAccessValues, JObject indexObject, List variableDefinitions, CancellationToken token) + public async Task Resolve(ElementAccessExpressionSyntax elementAccess, Dictionary memberAccessValues, JObject indexObject, List variableDefinitions, CancellationToken token) { try { @@ -413,9 +414,10 @@ public async Task Resolve(ElementAccessExpressionSyntax elementAccess, if (type == "string") { var eaExpressionFormatted = elementAccessStrExpression.Replace('.', '_'); // instance_str - variableDefinitions.Add(ExpressionEvaluator.ConvertJSToCSharpLocalVariableAssignment(eaExpressionFormatted, rootObject)); + variableDefinitions.Add(new (eaExpressionFormatted, rootObject, ExpressionEvaluator.ConvertJSToCSharpLocalVariableAssignment(eaExpressionFormatted, rootObject))); var eaFormatted = elementAccessStr.Replace('.', '_'); // instance_str[1] - return await ExpressionEvaluator.EvaluateSimpleExpression(this, eaFormatted, elementAccessStr, variableDefinitions, logger, token); + var variableDef = await ExpressionEvaluator.GetVariableDefinitions(this, variableDefinitions, invokeToStringInObject: false, token); + return await ExpressionEvaluator.EvaluateSimpleExpression(this, eaFormatted, elementAccessStr, variableDef, logger, token); } if (indexObject is null && elementIdxInfo.IndexingExpression is null) throw new InternalErrorException($"Unable to write index parameter to invoke the method in the runtime."); @@ -502,7 +504,8 @@ async Task GetElementIndexInfo() else { string expression = arg.ToString(); - indexObject = await ExpressionEvaluator.EvaluateSimpleExpression(this, expression, expression, variableDefinitions, logger, token); + var variableDef = await ExpressionEvaluator.GetVariableDefinitions(this, variableDefinitions, invokeToStringInObject: false, token); + indexObject = await ExpressionEvaluator.EvaluateSimpleExpression(this, expression, expression, variableDef, logger, token); string idxType = indexObject["type"].Value(); if (idxType != "number") throw new InvalidOperationException($"Cannot index with an object of type '{idxType}'"); diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs index f8023955317fda..0e261eac2b2da6 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs @@ -1933,14 +1933,14 @@ public Task InvokeMethod(DotnetObjectId dotnetObjectId, CancellationTok : throw new ArgumentException($"Cannot invoke method with id {methodId} on {dotnetObjectId}", nameof(dotnetObjectId)); } - public async Task InvokeToStringAsync(IEnumerable typeIds, bool isValueType, bool isEnum, int objectId, BindingFlags extraFlags, CancellationToken token) + public async Task InvokeToStringAsync(IEnumerable typeIds, bool isValueType, bool isEnum, int objectId, BindingFlags extraFlags, bool invokeToStringInObject, CancellationToken token) { try { foreach (var typeId in typeIds) { var typeInfo = await GetTypeInfo(typeId, token); - if (typeInfo == null || typeInfo.Name == "object") + if (typeInfo == null || (typeInfo.Name == "object" && !invokeToStringInObject)) continue; Microsoft.WebAssembly.Diagnostics.MethodInfo methodInfo = typeInfo.Info.Methods.FirstOrDefault(m => m.Name == "ToString"); if (isEnum != true && methodInfo == null) diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs b/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs index 0faca91f6713d9..9ca80c5c5b16ec 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs @@ -116,7 +116,7 @@ public async Task ToJObject(MonoSDBHelper sdbAgent, bool forDebuggerDis string description = className; if (ShouldAutoInvokeToString(className) || IsEnum) { - var toString = await sdbAgent.InvokeToStringAsync(new int[]{ TypeId }, isValueType: true, IsEnum, Id.Value, IsEnum ? BindingFlags.Default : BindingFlags.DeclaredOnly, token); + var toString = await sdbAgent.InvokeToStringAsync(new int[]{ TypeId }, isValueType: true, IsEnum, Id.Value, IsEnum ? BindingFlags.Default : BindingFlags.DeclaredOnly, invokeToStringInObject: false, token); if (toString == null) sdbAgent.logger.LogDebug($"Error while evaluating ToString method on typeId = {TypeId}"); else @@ -133,7 +133,7 @@ public async Task ToJObject(MonoSDBHelper sdbAgent, bool forDebuggerDis } else { - var toString = await sdbAgent.InvokeToStringAsync(new int[]{ TypeId }, isValueType: true, IsEnum, Id.Value, IsEnum ? BindingFlags.Default : BindingFlags.DeclaredOnly, token); + var toString = await sdbAgent.InvokeToStringAsync(new int[]{ TypeId }, isValueType: true, IsEnum, Id.Value, IsEnum ? BindingFlags.Default : BindingFlags.DeclaredOnly, invokeToStringInObject: false, token); if (toString != null) description = toString; } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrame2Tests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrame2Tests.cs index 25d77ed2285ef2..051da33469ce2d 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrame2Tests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrame2Tests.cs @@ -711,5 +711,25 @@ await EvaluateOnCallFrameAndCheck(id, ("EvaluateStaticGetterInValueType.A", TNumber(5)) ); }); + + [Fact] + public async Task EvaluateSumBetweenObjectAndString() => await CheckInspectLocalsAtBreakpointSite( + $"DebuggerTests.SumObjectAndString", "run", 7, "DebuggerTests.SumObjectAndString.run", + $"window.setTimeout(function() {{ invoke_static_method ('[debugger-test] DebuggerTests.SumObjectAndString:run'); 1 }})", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + await EvaluateOnCallFrameAndCheck(id, + ("myList+\"asd\"", TString("System.Collections.Generic.List`1[System.Int32]asd")), + ("dt+\"asd\"", TString("1/1/0001 12:00:00 AMasd")), + ("myClass+\"asd\"", TString("OverridenToStringasd")), + ("listNull+\"asd\"", TString("asd")) + ); + await CheckEvaluateFail(id, + ("myClass+dt", "Cannot evaluate '(myClass+dt\n)': (3,9): error CS0019: Operator '+' cannot be applied to operands of type 'object' and 'object'"), + ("myClass+1", "Cannot evaluate '(myClass+1\n)': (2,9): error CS0019: Operator '+' cannot be applied to operands of type 'object' and 'int'"), + ("dt+1", "Cannot evaluate '(dt+1\n)': (2,9): error CS0019: Operator '+' cannot be applied to operands of type 'object' and 'int'") + ); + }); } } 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 ac0785c437bc1a..480dc30115c430 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 @@ -2156,3 +2156,28 @@ public struct EvaluateStaticGetterInValueType { public static int A => 5; } + +namespace DebuggerTests +{ + public class SumObjectAndString + { + public class MyClass + { + public override string ToString() + { + return "OverridenToString"; + } + } + public static void run() + { + DateTime dt = new DateTime(); + List myList = new(); + List listNull = null; + object o = new(); + MyClass myClass = new(); + myList.Add(1); + Console.WriteLine(myList); + Console.WriteLine(dt); + } + } +} \ No newline at end of file