diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs index 00c551c36799df..4d9369d1ad0ae6 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs @@ -20,10 +20,11 @@ using Newtonsoft.Json.Linq; using System.Text.RegularExpressions; using System.Globalization; +using Microsoft.Extensions.Logging; namespace Microsoft.WebAssembly.Diagnostics { - internal static class EvaluateExpression + internal static class ExpressionEvaluator { internal static Script script = CSharpScript.Create( "", @@ -32,11 +33,11 @@ internal static class EvaluateExpression typeof(Enumerable).Assembly, typeof(JObject).Assembly )); - private sealed class FindVariableNMethodCall : CSharpSyntaxWalker + private sealed class ExpressionSyntaxReplacer : CSharpSyntaxWalker { private static Regex regexForReplaceVarName = new Regex(@"[^A-Za-z0-9_]", RegexOptions.Singleline); public List identifiers = new List(); - public List methodCall = new List(); + public List methodCalls = new List(); public List memberAccesses = new List(); public List elementAccess = new List(); public List argValues = new List(); @@ -78,7 +79,7 @@ public override void Visit(SyntaxNode node) if (node is InvocationExpressionSyntax) { if (visitCount == 1) - methodCall.Add(node as InvocationExpressionSyntax); + methodCalls.Add(node as InvocationExpressionSyntax); hasMethodCalls = true; } @@ -120,7 +121,7 @@ public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable ma_val }); // 1.1 Replace all this.a() occurrences with this_a_ABDE - root = root.ReplaceNodes(methodCall, (m, _) => + root = root.ReplaceNodes(methodCalls, (m, _) => { string iesStr = m.ToString(); if (!methodCallToParamName.TryGetValue(iesStr, out string id_name)) @@ -166,6 +167,8 @@ public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable ma_val memberAccessValues[id_name] = value; AddLocalVariableWithValue(id_name, value); } + // do not replace memberAccesses that were already replaced + memberAccesses = new List(); } if (id_values != null) @@ -178,7 +181,7 @@ public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable ma_val if (method_values != null) { - foreach ((InvocationExpressionSyntax ies, JObject value) in methodCall.Zip(method_values)) + foreach ((InvocationExpressionSyntax ies, JObject value) in methodCalls.Zip(method_values)) { string node_str = ies.ToString(); if (!methodCallToParamName.TryGetValue(node_str, out string id_name)) @@ -220,7 +223,6 @@ private static string ConvertJSToCSharpLocalVariableAssignment(string idName, JT JToken value = variable["value"]; string type = variable["type"].Value(); string subType = variable["subtype"]?.Value(); - string objectId = variable["objectId"]?.Value(); switch (type) { case "string": @@ -247,75 +249,123 @@ private static string ConvertJSToCSharpLocalVariableAssignment(string idName, JT typeRet = "bool"; break; case "object": - valueRet = "Newtonsoft.Json.Linq.JObject.FromObject(new {" - + $"type = \"{type}\"" - + $", description = \"{variable["description"].Value()}\"" - + $", className = \"{variable["className"].Value()}\"" - + (subType != null ? $", subtype = \"{subType}\"" : "") - + (objectId != null ? $", objectId = \"{objectId}\"" : "") - + "})"; - typeRet = "object"; + if (variable["subtype"]?.Value() == "null") + { + (valueRet, typeRet) = GetNullObject(variable["className"]?.Value()); + } + else + { + if (!DotnetObjectId.TryParse(variable["objectId"], out DotnetObjectId objectId)) + throw new Exception($"Internal error: Cannot parse objectId for var {idName}, with value: {variable}"); + + switch (objectId?.Scheme) + { + case "valuetype" when variable["isEnum"]?.Value() == true: + typeRet = variable["className"]?.Value(); + valueRet = $"({typeRet}) {value["value"].Value()}"; + break; + case "object": + default: + valueRet = "Newtonsoft.Json.Linq.JObject.FromObject(new {" + + $"type = \"{type}\"" + + $", description = \"{variable["description"].Value()}\"" + + $", className = \"{variable["className"].Value()}\"" + + (subType != null ? $", subtype = \"{subType}\"" : "") + + (objectId != null ? $", objectId = \"{objectId}\"" : "") + + "})"; + typeRet = "object"; + break; + } + } break; case "void": - valueRet = "Newtonsoft.Json.Linq.JObject.FromObject(new {" - + $"type = \"object\"," - + $"description = \"object\"," - + $"className = \"object\"," - + $"subtype = \"null\"" - + "})"; - typeRet = "object"; + (valueRet, typeRet) = GetNullObject("object"); break; default: throw new Exception($"Evaluate of this datatype {type} not implemented yet");//, "Unsupported"); } return $"{typeRet} {idName} = {valueRet};"; + + static (string, string) GetNullObject(string className = "object") + => ("Newtonsoft.Json.Linq.JObject.FromObject(new {" + + $"type = \"object\"," + + $"description = \"object\"," + + $"className = \"{className}\"," + + $"subtype = \"null\"" + + "})", + "object"); } } - private static async Task> ResolveMemberAccessExpressions(IEnumerable member_accesses, - MemberReferenceResolver resolver, CancellationToken token) + private static async Task> Resolve(IList collectionToResolve, MemberReferenceResolver resolver, + Func> resolutionFunc, CancellationToken token) { - var memberAccessValues = new List(); - foreach (MemberAccessExpressionSyntax maes in member_accesses) - { - string memberAccessString = maes.ToString(); - JObject value = await resolver.Resolve(memberAccessString, token); - if (value == null) - throw new ReturnAsErrorException($"Failed to resolve member access for {memberAccessString}", "ReferenceError"); - - memberAccessValues.Add(value); - } - - return memberAccessValues; + IList values = new List(); + foreach (T element in collectionToResolve) + values.Add(await resolutionFunc(element, resolver, token)); + return values; } - private static async Task> ResolveIdentifiers(IEnumerable identifiers, MemberReferenceResolver resolver, CancellationToken token) + private static async Task ResolveMemberAccessExpression(MemberAccessExpressionSyntax memberAccess, + MemberReferenceResolver resolver, CancellationToken token) { - var values = new List(); - foreach (IdentifierNameSyntax var in identifiers) - { - JObject value = await resolver.Resolve(var.Identifier.Text, token); - if (value == null) - throw new ReturnAsErrorException($"The name {var.Identifier.Text} does not exist in the current context", "ReferenceError"); - - values.Add(value); - } + string memberAccessString = memberAccess.ToString(); + JObject value = await resolver.Resolve(memberAccessString, token); + return value ?? throw new ReturnAsErrorException($"Failed to resolve member access for {memberAccessString}", "ReferenceError"); + } - return values; + private static async Task ResolveIdentifier(IdentifierNameSyntax identifier, + MemberReferenceResolver resolver, CancellationToken token) + { + JObject value = await resolver.Resolve(identifier.Identifier.Text, token); + return value ?? throw new ReturnAsErrorException($"The name {identifier.Identifier.Text} does not exist in the current context", "ReferenceError"); } - private static async Task> ResolveMethodCalls(IEnumerable methodCalls, Dictionary memberAccessValues, MemberReferenceResolver resolver, CancellationToken token) + private static async Task<(IList, IList, IList)> ResolveMethodCalls(ExpressionSyntaxReplacer replacer, MemberReferenceResolver resolver, CancellationToken token) { - var values = new List(); - foreach (InvocationExpressionSyntax methodCall in methodCalls) + var methodCallValues = new List(capacity: replacer.methodCalls.Count); + // used for replacing method call on primitive: + var maesValues = new List(capacity: replacer.methodCalls.Count); + var identifierValues = new List(capacity: replacer.methodCalls.Count); + InvocationExpressionSyntax[] methodCallsCopy = replacer.methodCalls.ToArray(); + foreach (InvocationExpressionSyntax methodCall in methodCallsCopy) { - JObject value = await resolver.Resolve(methodCall, memberAccessValues, token); + JObject value = await resolver.Resolve(methodCall, replacer.memberAccessValues, token); if (value == null) - throw new ReturnAsErrorException($"Failed to resolve member access for {methodCall}", "ReferenceError"); + { + await ReplaceMethodCall(methodCall); + continue; + } + methodCallValues.Add(value); + } + return (methodCallValues, maesValues, identifierValues); - values.Add(value); + async Task ReplaceMethodCall(InvocationExpressionSyntax method) + { + /* + Instead of invoking the method on the primitive type in the runtime, + we emit a local for the primitive, and emit the the method call + itself in the script. For example: + double test_propUlong_2c64c = 12; + return (test_propUlong_2c64c.ToString()); + */ + replacer.methodCalls.Remove(method); + if (method.Expression is MemberAccessExpressionSyntax mses) + { + // primitive is a member field: + if (mses.Expression is MemberAccessExpressionSyntax msesExpr) + { + replacer.memberAccesses.Add(msesExpr); + maesValues.Add(await ResolveMemberAccessExpression(msesExpr, resolver, token)); + } + // primitive is a local value: + else if (mses.Expression is IdentifierNameSyntax identifierExpr) + { + replacer.identifiers.Add(identifierExpr); + identifierValues.Add(await ResolveIdentifier(identifierExpr, resolver, token)); + } + } } - return values; } private static async Task> ResolveElementAccess(IEnumerable elementAccesses, Dictionary memberAccessValues, MemberReferenceResolver resolver, CancellationToken token) @@ -332,7 +382,8 @@ private static async Task> ResolveElementAccess(IEnumerable CompileAndRunTheExpression(string expression, MemberReferenceResolver resolver, CancellationToken token) + internal static async Task CompileAndRunTheExpression( + string expression, MemberReferenceResolver resolver, ILogger logger, CancellationToken token) { expression = expression.Trim(); if (!expression.StartsWith('(')) @@ -344,8 +395,8 @@ internal static async Task CompileAndRunTheExpression(string expression SyntaxNode expressionTree = syntaxTree.GetCompilationUnitRoot(token); if (expressionTree == null) throw new Exception($"BUG: Unable to evaluate {expression}, could not get expression from the syntax tree"); - FindVariableNMethodCall findVarNMethodCall = new FindVariableNMethodCall(); - findVarNMethodCall.VisitInternal(expressionTree); + ExpressionSyntaxReplacer replacer = new ExpressionSyntaxReplacer(); + replacer.VisitInternal(expressionTree); // this fails with `"a)"` // because the code becomes: return (a)); // and the returned expression from GetExpressionFromSyntaxTree is `a`! @@ -359,55 +410,54 @@ internal static async Task CompileAndRunTheExpression(string expression return value; } - IList memberAccessValues = await ResolveMemberAccessExpressions(findVarNMethodCall.memberAccesses, resolver, token); + IList memberAccessValues = await Resolve(replacer.memberAccesses, resolver, ResolveMemberAccessExpression, token); + IList identifierValues = await Resolve(replacer.identifiers, resolver, ResolveIdentifier, token); + syntaxTree = replacer.ReplaceVars(syntaxTree, memberAccessValues, identifierValues, null, null); // eg. "this.dateTime", " dateTime.TimeOfDay" - if (expressionTree.Kind() == SyntaxKind.SimpleMemberAccessExpression && findVarNMethodCall.memberAccesses.Count == 1) + if (expressionTree.Kind() == SyntaxKind.SimpleMemberAccessExpression && replacer.memberAccesses.Count == 1) { return memberAccessValues[0]; } - IList identifierValues = await ResolveIdentifiers(findVarNMethodCall.identifiers, resolver, token); - - syntaxTree = findVarNMethodCall.ReplaceVars(syntaxTree, memberAccessValues, identifierValues, null, null); - - if (findVarNMethodCall.hasMethodCalls) + if (replacer.hasMethodCalls) { expressionTree = syntaxTree.GetCompilationUnitRoot(token); - findVarNMethodCall.VisitInternal(expressionTree); + replacer.VisitInternal(expressionTree); - IList methodValues = await ResolveMethodCalls(findVarNMethodCall.methodCall, findVarNMethodCall.memberAccessValues, resolver, token); - - syntaxTree = findVarNMethodCall.ReplaceVars(syntaxTree, null, null, methodValues, null); + (IList methodValues, IList newMemberAccessValues, IList newIdentifierValues) = + await ResolveMethodCalls(replacer, resolver, token); + syntaxTree = replacer.ReplaceVars(syntaxTree, newMemberAccessValues, newIdentifierValues, methodValues, null); } // eg. "elements[0]" - if (findVarNMethodCall.hasElementAccesses) + if (replacer.hasElementAccesses) { expressionTree = syntaxTree.GetCompilationUnitRoot(token); - findVarNMethodCall.VisitInternal(expressionTree); + replacer.VisitInternal(expressionTree); - IList elementAccessValues = await ResolveElementAccess(findVarNMethodCall.elementAccess, findVarNMethodCall.memberAccessValues, resolver, token); + IList elementAccessValues = await ResolveElementAccess(replacer.elementAccess, replacer.memberAccessValues, resolver, token); - syntaxTree = findVarNMethodCall.ReplaceVars(syntaxTree, null, null, null, elementAccessValues); + 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"); + Script newScript = script; try { - var newScript = script.ContinueWith( - string.Join("\n", findVarNMethodCall.variableDefinitions) + "\nreturn " + syntaxTree.ToString()); - + newScript = script.ContinueWith( + string.Join("\n", replacer.variableDefinitions) + "\nreturn " + syntaxTree.ToString()); var state = await newScript.RunAsync(cancellationToken: token); return JObject.FromObject(ConvertCSharpToJSType(state.ReturnValue, state.ReturnValue?.GetType())); } catch (CompilationErrorException cee) { + logger.LogDebug($"Cannot evaluate '{expression}'. Script used to compile it: {newScript.Code}{Environment.NewLine}{cee.Message}"); throw new ReturnAsErrorException($"Cannot evaluate '{expression}': {cee.Message}", "CompilationError"); } catch (Exception ex) @@ -434,6 +484,8 @@ private static object ConvertCSharpToJSType(object v, Type type) 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; return new { type = "object", value = v, description = v.ToString(), className = type.ToString() }; diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs index 607a9be796bd5a..c7acb513957c45 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs @@ -606,7 +606,7 @@ protected override async Task AcceptCommand(MessageId sessionId, JObject a var resolver = new MemberReferenceResolver(this, context, sessionId, scope.Id, logger); JObject retValue = await resolver.Resolve(args?["text"]?.Value(), token); if (retValue == null) - retValue = await EvaluateExpression.CompileAndRunTheExpression(args?["text"]?.Value(), resolver, token); + retValue = await ExpressionEvaluator.CompileAndRunTheExpression(args?["text"]?.Value(), resolver, logger, token); var osend = JObject.FromObject(new { type = "evaluationResult", diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs index 17690f88c9e0f8..724953fab2ce85 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs @@ -92,7 +92,7 @@ public async Task GetValueFromObject(JToken objRet, CancellationToken t if (typeId != -1) { - JObject memberObject = await FindStaticMemberInType(part, typeId); + JObject memberObject = await FindStaticMemberInType(classNameToFind, part, typeId); if (memberObject != null) { string remaining = null; @@ -124,7 +124,7 @@ public async Task GetValueFromObject(JToken objRet, CancellationToken t return (null, null); - async Task FindStaticMemberInType(string name, int typeId) + async Task FindStaticMemberInType(string classNameToFind, string name, int typeId) { var fields = await context.SdbAgent.GetTypeFields(typeId, token); foreach (var field in fields) @@ -137,9 +137,11 @@ async Task FindStaticMemberInType(string name, int typeId) { isInitialized = await context.SdbAgent.TypeInitialize(typeId, token); } - var valueRet = await context.SdbAgent.GetFieldValue(typeId, field.Id, token); - - return await GetValueFromObject(valueRet, token); + var staticFieldValue = await context.SdbAgent.GetFieldValue(typeId, field.Id, token); + var valueRet = await GetValueFromObject(staticFieldValue, token); + // we need the full name here + valueRet["className"] = classNameToFind; + return valueRet; } var methodId = await context.SdbAgent.GetPropertyMethodIdByName(typeId, name, token); @@ -375,125 +377,126 @@ public async Task Resolve(ElementAccessExpressionSyntax elementAccess, } } - public async Task Resolve(InvocationExpressionSyntax method, Dictionary memberAccessValues, CancellationToken token) + public async Task<(JObject, string)> ResolveInvokationInfo(InvocationExpressionSyntax method, CancellationToken token) { var methodName = ""; - bool isExtensionMethod = false; try { JObject rootObject = null; var expr = method.Expression; - if (expr is MemberAccessExpressionSyntax) + if (expr is MemberAccessExpressionSyntax memberAccessExpressionSyntax) { - var memberAccessExpressionSyntax = expr as MemberAccessExpressionSyntax; rootObject = await Resolve(memberAccessExpressionSyntax.Expression.ToString(), token); methodName = memberAccessExpressionSyntax.Name.ToString(); if (rootObject.IsNullValuedObject()) throw new ExpressionEvaluationFailedException($"Expression '{memberAccessExpressionSyntax}' evaluated to null"); } - else if (expr is IdentifierNameSyntax) + else if (expr is IdentifierNameSyntax && scopeCache.ObjectFields.TryGetValue("this", out JObject thisValue)) { - if (scopeCache.ObjectFields.TryGetValue("this", out JObject valueRet)) - { - rootObject = await GetValueFromObject(valueRet, token); - methodName = expr.ToString(); - } + rootObject = await GetValueFromObject(thisValue, token); + methodName = expr.ToString(); } + return (rootObject, methodName); + } + catch (Exception ex) when (ex is not (ExpressionEvaluationFailedException or ReturnAsErrorException)) + { + throw new Exception($"Unable to evaluate method '{methodName}'", ex); + } + } - if (rootObject != null) + private static readonly string[] primitiveTypes = new string[] { "string", "number", "boolean", "symbol" }; + + public async Task Resolve(InvocationExpressionSyntax method, Dictionary memberAccessValues, CancellationToken token) + { + (JObject rootObject, string methodName) = await ResolveInvokationInfo(method, token); + if (rootObject == null) + throw new ReturnAsErrorException($"Failed to resolve root object for {method}", "ReferenceError"); + + // primitives don't have objectId + if (!DotnetObjectId.TryParse(rootObject["objectId"]?.Value(), out DotnetObjectId objectId) && + primitiveTypes.Contains(rootObject["type"]?.Value())) + return null; + + if (method.ArgumentList == null) + throw new InternalErrorException($"Failed to resolve method call for {method}, list of arguments is null."); + + bool isExtensionMethod = false; + try + { + var typeIds = await context.SdbAgent.GetTypeIdsForObject(objectId.Value, true, token); + int methodId = await context.SdbAgent.GetMethodIdByName(typeIds[0], methodName, token); + var className = await context.SdbAgent.GetTypeNameOriginal(typeIds[0], token); + if (methodId == 0) //try to search on System.Linq.Enumerable + methodId = await FindMethodIdOnLinqEnumerable(typeIds, methodName); + + if (methodId == 0) + { + var typeName = await context.SdbAgent.GetTypeName(typeIds[0], token); + throw new ReturnAsErrorException($"Method '{methodName}' not found in type '{typeName}'", "ReferenceError"); + } + using var commandParamsObjWriter = new MonoBinaryWriter(); + if (!isExtensionMethod) { - if (!DotnetObjectId.TryParse(rootObject?["objectId"]?.Value(), out DotnetObjectId objectId)) - throw new ExpressionEvaluationFailedException($"Cannot invoke method '{methodName}' on invalid object id: {rootObject}"); + // instance method + commandParamsObjWriter.WriteObj(objectId, context.SdbAgent); + } - List typeIds; - if (objectId.IsValueType) + int passedArgsCnt = method.ArgumentList.Arguments.Count; + int methodParamsCnt = passedArgsCnt; + ParameterInfo[] methodParamsInfo = null; + var methodInfo = await context.SdbAgent.GetMethodInfo(methodId, token); + if (methodInfo != null) + { + methodParamsInfo = methodInfo.Info.GetParametersInfo(); + methodParamsCnt = methodParamsInfo.Length; + if (isExtensionMethod) { - if (!context.SdbAgent.valueTypes.TryGetValue(objectId.Value, out ValueTypeClass valueType)) - throw new Exception($"Could not find valuetype {objectId}"); - - typeIds = new List(1) { valueType.TypeId }; + // implicit *this* parameter + methodParamsCnt--; } - else + if (passedArgsCnt > methodParamsCnt) + throw new ReturnAsErrorException($"Unable to evaluate method '{methodName}'. Too many arguments passed.", "ArgumentError"); + } + + if (isExtensionMethod) + { + commandParamsObjWriter.Write(methodParamsCnt + 1); + commandParamsObjWriter.WriteObj(objectId, context.SdbAgent); + } + else + { + commandParamsObjWriter.Write(methodParamsCnt); + } + + int argIndex = 0; + // explicitly passed arguments + for (; argIndex < passedArgsCnt; argIndex++) + { + var arg = method.ArgumentList.Arguments[argIndex]; + if (arg.Expression is LiteralExpressionSyntax literal) { - typeIds = await context.SdbAgent.GetTypeIdsForObject(objectId.Value, true, token); + if (!await commandParamsObjWriter.WriteConst(literal, context.SdbAgent, token)) + throw new InternalErrorException($"Unable to evaluate method '{methodName}'. Unable to write LiteralExpressionSyntax into binary writer."); } - int methodId = await context.SdbAgent.GetMethodIdByName(typeIds[0], methodName, token); - var className = await context.SdbAgent.GetTypeNameOriginal(typeIds[0], token); - if (methodId == 0) //try to search on System.Linq.Enumerable - methodId = await FindMethodIdOnLinqEnumerable(typeIds, methodName); - - if (methodId == 0) { - var typeName = await context.SdbAgent.GetTypeName(typeIds[0], token); - throw new ExpressionEvaluationFailedException($"Method '{methodName}' not found in type '{typeName}'"); - } - using var commandParamsObjWriter = new MonoBinaryWriter(); - if (!isExtensionMethod) + else if (arg.Expression is IdentifierNameSyntax identifierName) { - // instance method - commandParamsObjWriter.WriteObj(objectId, context.SdbAgent); + if (!await commandParamsObjWriter.WriteJsonValue(memberAccessValues[identifierName.Identifier.Text], context.SdbAgent, token)) + throw new InternalErrorException($"Unable to evaluate method '{methodName}'. Unable to write IdentifierNameSyntax into binary writer."); } - - if (method.ArgumentList != null) + else { - int passedArgsCnt = method.ArgumentList.Arguments.Count; - int methodParamsCnt = passedArgsCnt; - ParameterInfo[] methodParamsInfo = null; - var methodInfo = await context.SdbAgent.GetMethodInfo(methodId, token); - if (methodInfo != null) //FIXME: #65670 - { - methodParamsInfo = methodInfo.Info.GetParametersInfo(); - methodParamsCnt = methodParamsInfo.Length; - if (isExtensionMethod) - { - // implicit *this* parameter - methodParamsCnt--; - } - if (passedArgsCnt > methodParamsCnt) - throw new ExpressionEvaluationFailedException($"Cannot invoke method '{method}' - too many arguments passed"); - } - - if (isExtensionMethod) - { - commandParamsObjWriter.Write(methodParamsCnt + 1); - commandParamsObjWriter.WriteObj(objectId, context.SdbAgent); - } - else - { - commandParamsObjWriter.Write(methodParamsCnt); - } - - int argIndex = 0; - // explicitly passed arguments - for (; argIndex < passedArgsCnt; argIndex++) - { - var arg = method.ArgumentList.Arguments[argIndex]; - if (arg.Expression is LiteralExpressionSyntax literal) - { - if (!await commandParamsObjWriter.WriteConst(literal, context.SdbAgent, token)) - throw new InternalErrorException($"Unable to write LiteralExpressionSyntax ({literal}) into binary writer."); - } - else if (arg.Expression is IdentifierNameSyntax identifierName) - { - if (!await commandParamsObjWriter.WriteJsonValue(memberAccessValues[identifierName.Identifier.Text], context.SdbAgent, token)) - throw new InternalErrorException($"Unable to write IdentifierNameSyntax ({identifierName}) into binary writer."); - } - else - { - throw new InternalErrorException($"Unable to write into binary writer, not recognized expression type: {arg.Expression.GetType().Name}"); - } - } - // optional arguments that were not overwritten - for (; argIndex < methodParamsCnt; argIndex++) - { - if (!await commandParamsObjWriter.WriteConst(methodParamsInfo[argIndex].TypeCode, methodParamsInfo[argIndex].Value, context.SdbAgent, token)) - throw new InternalErrorException($"Unable to write optional parameter {methodParamsInfo[argIndex].Name} value in method '{methodName}' to the mono buffer."); - } - var retMethod = await context.SdbAgent.InvokeMethod(commandParamsObjWriter.GetParameterBuffer(), methodId, token); - return await GetValueFromObject(retMethod, token); + throw new InternalErrorException($"Unable to evaluate method '{methodName}'. Unable to write into binary writer, not recognized expression type: {arg.Expression.GetType().Name}"); } } - return null; + // optional arguments that were not overwritten + for (; argIndex < methodParamsCnt; argIndex++) + { + if (!await commandParamsObjWriter.WriteConst(methodParamsInfo[argIndex].TypeCode, methodParamsInfo[argIndex].Value, context.SdbAgent, token)) + throw new InternalErrorException($"Unable to write optional parameter {methodParamsInfo[argIndex].Name} value in method '{methodName}' to the mono buffer."); + } + var retMethod = await context.SdbAgent.InvokeMethod(commandParamsObjWriter.GetParameterBuffer(), methodId, token); + return await GetValueFromObject(retMethod, token); } catch (Exception ex) when (ex is not (ExpressionEvaluationFailedException or ReturnAsErrorException)) { diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs index 4dc9d03892dcac..56eadec81ce063 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -821,7 +821,7 @@ protected async Task EvaluateCondition(SessionId sessionId, ExecutionConte var resolver = new MemberReferenceResolver(this, context, sessionId, mono_frame.Id, logger); JObject retValue = await resolver.Resolve(condition, token); if (retValue == null) - retValue = await EvaluateExpression.CompileAndRunTheExpression(condition, resolver, token); + retValue = await ExpressionEvaluator.CompileAndRunTheExpression(condition, resolver, logger, token); if (retValue?["value"]?.Type == JTokenType.Boolean || retValue?["value"]?.Type == JTokenType.Integer || retValue?["value"]?.Type == JTokenType.Float) { @@ -1314,7 +1314,7 @@ private async Task OnEvaluateOnCallFrame(MessageId msg_id, int scopeId, st JObject retValue = await resolver.Resolve(expression, token); if (retValue == null) { - retValue = await EvaluateExpression.CompileAndRunTheExpression(expression, resolver, token); + retValue = await ExpressionEvaluator.CompileAndRunTheExpression(expression, resolver, logger, token); } if (retValue != null) diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs index 7721666ff24b32..809d310be82329 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs @@ -1430,7 +1430,7 @@ public async Task GetValueFromDebuggerDisplayAttribute(DotnetObjectId do expr = "$\"" + dispAttrStr + "\""; JObject retValue = await resolver.Resolve(expr, token); if (retValue == null) - retValue = await EvaluateExpression.CompileAndRunTheExpression(expr, resolver, token); + retValue = await ExpressionEvaluator.CompileAndRunTheExpression(expr, resolver, logger, token); return retValue?["value"]?.Value(); } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs b/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs index a9732a5224bcc6..1444e6d55d52dc 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs @@ -114,9 +114,15 @@ public async Task ToJObject(MonoSDBHelper sdbAgent, bool forDebuggerDis if (displayString != null) description = displayString; } - - var obj = MonoSDBHelper.CreateJObject(null, "object", description, false, className, Id.ToString(), null, null, true, true, IsEnum); - return obj; + return MonoSDBHelper.CreateJObject( + IsEnum ? fields[0]["value"] : null, + "object", + description, + false, + className, + Id.ToString(), + null, null, true, true, + IsEnum); } public async Task GetProxy(MonoSDBHelper sdbHelper, CancellationToken token) diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestFirefox.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestFirefox.cs index 884054c78963d1..e5712b232e191d 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestFirefox.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestFirefox.cs @@ -422,7 +422,7 @@ internal override async Task SetBreakpointInMethod(string assembly, stri description = res.Value["result"]["value"]["description"], objectId = actor }); - if (actor.StartsWith("dotnet:valuetype:")) + if (actor?.StartsWith("dotnet:valuetype:") == true) resObj["isValueType"] = true; return (resObj, res); } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs index 1b693c113443b8..7a8133ba28eb70 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs @@ -506,19 +506,25 @@ public async Task EvaluateSimpleMethodCallsError() => await CheckInspectLocalsAt var id = pause_location["callFrames"][0]["callFrameId"].Value(); var (_, res) = await EvaluateOnCallFrame(id, "this.objToTest.MyMethodWrong()", expect_ok: false ); - Assert.Contains($"Method 'MyMethodWrong' not found", res.Error["message"]?.Value()); + Assert.Equal( + $"Method 'MyMethodWrong' not found in type 'DebuggerTests.EvaluateMethodTestsClass.ParmToTest'", + res.Error["result"]?["description"]?.Value()); (_, res) = await EvaluateOnCallFrame(id, "this.objToTest.MyMethod(1)", expect_ok: false); - Assert.Contains("Cannot invoke method 'this.objToTest.MyMethod(1)' - too many arguments passed", res.Error["message"]?.Value()); + Assert.Equal( + "Unable to evaluate method 'MyMethod'. Too many arguments passed.", + res.Error["result"]?["description"]?.Value()); (_, res) = await EvaluateOnCallFrame(id, "this.CallMethodWithParm(\"1\")", expect_ok: false ); Assert.Contains("Unable to evaluate method 'this.CallMethodWithParm(\"1\")'", res.Error["message"]?.Value()); (_, res) = await EvaluateOnCallFrame(id, "this.ParmToTestObjNull.MyMethod()", expect_ok: false ); - Assert.Contains("Expression 'this.ParmToTestObjNull.MyMethod' evaluated to null", res.Error["message"]?.Value()); + Assert.Equal("Expression 'this.ParmToTestObjNull.MyMethod' evaluated to null", res.Error["message"]?.Value()); (_, res) = await EvaluateOnCallFrame(id, "this.ParmToTestObjException.MyMethod()", expect_ok: false ); - Assert.Contains("Cannot invoke method 'MyMethod'", res.Error["message"]?.Value()); + Assert.Contains( + "Cannot evaluate '(this.ParmToTestObjException.MyMethod()\n)'", + res.Error["result"]?["description"]?.Value()); }); [Fact] @@ -1105,7 +1111,7 @@ await EvaluateOnCallFrameAndCheck(id, ("test.GetString()", TString("1.23")), ("test.GetUnicodeString()", TString("żółć")), - ("test.GetString(null)", TObject("string", is_null: true)), + ("test.GetString(null)", TString(null)), ("test.GetStringNullable()", TString("1.23")), ("test.GetSingle()", JObject.FromObject( new { type = "number", value = 1.23, description = "1.23" })), @@ -1113,9 +1119,9 @@ await EvaluateOnCallFrameAndCheck(id, ("test.GetSingleNullable()", JObject.FromObject( new { type = "number", value = 1.23, description = "1.23" })), ("test.GetDoubleNullable()", JObject.FromObject( new { type = "number", value = 1.23, description = "1.23" })), - ("test.GetBool()", JObject.FromObject( new { type = "object", value = true, description = "True", className = "System.Boolean" })), - ("test.GetBoolNullable()", JObject.FromObject( new { type = "object", value = true, description = "True", className = "System.Boolean" })), - ("test.GetNull()", JObject.FromObject( new { type = "object", value = true, description = "True", className = "System.Boolean" })), + ("test.GetBool()", TBool(true)), + ("test.GetBoolNullable()", TBool(true)), + ("test.GetNull()", TBool(true)), ("test.GetDefaultAndRequiredParam(2)", TNumber(5)), ("test.GetDefaultAndRequiredParam(3, 2)", TNumber(5)), @@ -1125,7 +1131,9 @@ await EvaluateOnCallFrameAndCheck(id, ); var (_, res) = await EvaluateOnCallFrame(id, "test.GetDefaultAndRequiredParamMixedTypes(\"a\", 23, true, 1.23f)", expect_ok: false); - Assert.Contains("method 'test.GetDefaultAndRequiredParamMixedTypes(\"a\", 23, true, 1.23f)' - too many arguments passed", res.Error["message"]?.Value()); + Assert.Equal( + "Unable to evaluate method 'GetDefaultAndRequiredParamMixedTypes'. Too many arguments passed.", + res.Error["exceptionDetails"]["exception"]["description"]?.Value()); }); [Fact] @@ -1139,6 +1147,41 @@ await EvaluateOnCallFrameAndCheck(id, ("test.listToLinq.ToList()", TObject("System.Collections.Generic.List", description: "Count = 11")) ); }); - } + [Fact] + public async Task EvaluateMethodsOnPrimitiveTypesReturningPrimitives() => 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(); + await EvaluateOnCallFrameAndCheck(id, + ("test.propInt.ToString()", TString("12")), + ("test.propUint.ToString()", TString("12")), + ("test.propLong.ToString()", TString("12")), + ("test.propUlong.ToString()", TString("12")), + // ("test.propFloat.ToString()", TString("1.2345678")), // is comma, should be dot, fixed in PR #66979 + // ("test.propDouble.ToString()", TString("1.2345678910111213")), // is comma, should be dot + ("test.propBool.ToString()", TString("True")), + ("test.propChar.ToString()", TString("X")), + ("test.propString.ToString()", TString("s_t_r")), + ("test.propString.Split('*', 3, System.StringSplitOptions.RemoveEmptyEntries)", TObject("System.String[]")), + ("test.propString.EndsWith('r')", TBool(true)), + ("test.propString.StartsWith('S')", TBool(false)), + ("localInt.ToString()", TString("2")), + ("localUint.ToString()", TString("2")), + ("localLong.ToString()", TString("2")), + ("localUlong.ToString()", TString("2")), + // ("localFloat.ToString()", TString("0.2345678")), // is comma, should be dot + // ("localDouble.ToString()", TString("0.2345678910111213")), // is comma, should be dot + ("localBool.ToString()", TString("False")), + ("localBool.GetHashCode()", TNumber(0)), + ("localBool.GetTypeCode()", TObject("System.TypeCode", "Boolean")), + ("localChar.ToString()", TString("Y")), + ("localString.ToString()", TString("S*T*R")), + ("localString.Split('*', 3, System.StringSplitOptions.TrimEntries)", TObject("System.String[]")), + ("localString.EndsWith('r')", TBool(false)), + ("localString.StartsWith('S')", TBool(true))); + }); + } } 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 ef663949f718c2..e7f2986f9db1a6 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 @@ -1371,6 +1371,36 @@ public static void Evaluate() var test = new TestClass(); } } + + public static class PrimitiveTypeMethods + { + public class TestClass + { + public int propInt = 12; + public uint propUint = 12; + public long propLong = 12; + public ulong propUlong = 12; + public float propFloat = 1.2345678f; + public double propDouble = 1.2345678910111213; + public bool propBool = true; + public char propChar = 'X'; + public string propString = "s_t_r"; + } + + public static void Evaluate() + { + var test = new TestClass(); + int localInt = 2; + uint localUint = 2; + long localLong = 2; + ulong localUlong = 2; + float localFloat = 0.2345678f; + double localDouble = 0.2345678910111213; + bool localBool = false; + char localChar = 'Y'; + string localString = "S*T*R"; + } + } } namespace DebuggerTestsV2