From 778137b0d31ed47731a3e0429f1b05de33de482b Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> Date: Thu, 21 Jul 2022 13:11:59 +0200 Subject: [PATCH] Enable string properties evaluation of Length and Char[]. (#67028) * Testcases. * Fixed indexing property and length property. * Fixed changes from https://github.com/dotnet/runtime/pull/67095 that I broke sometime when merging. * Granted objectId to string: properties and methods on strings are evaluated the similarly as on objects. * Removed the comments. * Fixed EvaluateMethodsOnPrimitiveTypesReturningObjects on Firefox. * Disabled firefox test https://github.com/dotnet/runtime/issues/70819. * Fixed the test build error. * Full names to fix the tests. --- .../BrowserDebugProxy/EvaluateExpression.cs | 174 ++++++------- .../Firefox/FirefoxMonoProxy.cs | 2 +- .../BrowserDebugProxy/JObjectValueCreator.cs | 6 +- .../MemberReferenceResolver.cs | 242 +++++++++++------- .../BrowserDebugProxy/MonoSDBHelper.cs | 35 ++- .../BrowserDebugProxy/ValueTypeClass.cs | 6 +- .../DebuggerTestSuite/DebuggerTestFirefox.cs | 73 +++--- .../EvaluateOnCallFrameTests.cs | 92 ++++--- .../debugger-test/debugger-evaluate-test.cs | 16 +- 9 files changed, 389 insertions(+), 257 deletions(-) diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs index c0fee6a7609055..d79bf66319033f 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs @@ -215,17 +215,18 @@ void AddLocalVariableWithValue(string idName, JObject value) variableDefinitions.Add(ConvertJSToCSharpLocalVariableAssignment(idName, value)); } } + } - private static string ConvertJSToCSharpLocalVariableAssignment(string idName, JToken variable) + public static string ConvertJSToCSharpLocalVariableAssignment(string idName, JToken variable) + { + string typeRet; + object valueRet; + JToken value = variable["value"]; + string type = variable["type"].Value(); + string subType = variable["subtype"]?.Value(); + switch (type) { - string typeRet; - object valueRet; - JToken value = variable["value"]; - string type = variable["type"].Value(); - string subType = variable["subtype"]?.Value(); - switch (type) - { - case "string": + case "string": { var str = value?.Value(); str = str.Replace("\"", "\\\""); @@ -233,68 +234,67 @@ private static string ConvertJSToCSharpLocalVariableAssignment(string idName, JT typeRet = "string"; break; } - case "symbol": - { - valueRet = $"'{value?.Value()}'"; - typeRet = "char"; - break; - } - case "number": - //casting to double and back to string would loose precision; so casting straight to string - valueRet = value?.Value(); - typeRet = "double"; - break; - case "boolean": - valueRet = value?.Value().ToLowerInvariant(); - typeRet = "bool"; + case "symbol": + { + valueRet = $"'{value?.Value()}'"; + typeRet = "char"; break; - case "object": - if (variable["subtype"]?.Value() == "null") - { - (valueRet, typeRet) = GetNullObject(variable["className"]?.Value()); - } - else + } + case "number": + //casting to double and back to string would loose precision; so casting straight to string + valueRet = value?.Value(); + typeRet = "double"; + break; + case "boolean": + valueRet = value?.Value().ToLowerInvariant(); + typeRet = "bool"; + break; + case "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) { - 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; - } + 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, 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"); + } + break; + case "void": + (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> Resolve(IList collectionToResolve, MemberReferenceResolver resolver, @@ -368,13 +368,14 @@ async Task ReplaceMethodCall(InvocationExpressionSyntax method) } } - private static async Task> ResolveElementAccess(IEnumerable elementAccesses, Dictionary memberAccessValues, MemberReferenceResolver resolver, CancellationToken token) + private static async Task> ResolveElementAccess(ExpressionSyntaxReplacer replacer, MemberReferenceResolver resolver, CancellationToken token) { var values = new List(); JObject index = null; + IEnumerable elementAccesses = replacer.elementAccess; foreach (ElementAccessExpressionSyntax elementAccess in elementAccesses.Reverse()) { - index = await resolver.Resolve(elementAccess, memberAccessValues, index, token); + index = await resolver.Resolve(elementAccess, replacer.memberAccessValues, index, replacer.variableDefinitions, token); if (index == null) throw new ReturnAsErrorException($"Failed to resolve element access for {elementAccess}", "ReferenceError"); } @@ -438,7 +439,7 @@ internal static async Task CompileAndRunTheExpression( replacer.VisitInternal(expressionTree); - IList elementAccessValues = await ResolveElementAccess(replacer.elementAccess, replacer.memberAccessValues, resolver, token); + IList elementAccessValues = await ResolveElementAccess(replacer, resolver, token); syntaxTree = replacer.ReplaceVars(syntaxTree, null, null, null, elementAccessValues); } @@ -447,42 +448,29 @@ internal static async Task CompileAndRunTheExpression( if (expressionTree == null) throw new Exception($"BUG: Unable to evaluate {expression}, could not get expression from the syntax tree"); + return await EvaluateSimpleExpression(resolver, syntaxTree.ToString(), expression, replacer.variableDefinitions, logger, token); + } + + internal static async Task EvaluateSimpleExpression( + MemberReferenceResolver resolver, string compiledExpression, string orginalExpression, List variableDefinitions, ILogger logger, CancellationToken token) + { Script newScript = script; try { - newScript = script.ContinueWith( - string.Join("\n", replacer.variableDefinitions) + "\nreturn " + syntaxTree.ToString()); + newScript = script.ContinueWith(string.Join("\n", variableDefinitions) + "\nreturn " + compiledExpression + ";"); var state = await newScript.RunAsync(cancellationToken: token); return JObject.FromObject(resolver.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"); + logger.LogDebug($"Cannot evaluate '{orginalExpression}'. Script used to compile it: {newScript.Code}{Environment.NewLine}{cee.Message}"); + throw new ReturnAsErrorException($"Cannot evaluate '{orginalExpression}': {cee.Message}", "CompilationError"); } catch (Exception ex) { - throw new Exception($"Internal Error: Unable to run {expression}, error: {ex.Message}.", ex); + throw new Exception($"Internal Error: Unable to run {orginalExpression}, error: {ex.Message}.", ex); } } - - private static JObject ConvertCLRToJSType(object v) - { - if (v is JObject jobj) - return jobj; - - if (v is null) - return JObjectValueCreator.CreateNull("")?["value"] as JObject; - - string typeName = v.GetType().ToString(); - jobj = JObjectValueCreator.CreateFromPrimitiveType(v); - return jobj is not null - ? jobj["value"] as JObject - : JObjectValueCreator.Create(value: null, - type: "object", - description: v.ToString(), - className: typeName)?["value"] as JObject; - } } internal sealed class ReturnAsErrorException : Exception diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs index c34689b5e9e090..deb9761130b3cc 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs @@ -772,7 +772,7 @@ private static JObject ConvertToFirefoxContent(ValueOrError re @class = variable["value"]?["className"]?.Value(), value = variable["value"]?["description"]?.Value(), actor = variable["value"]["objectId"].Value(), - type = "object" + type = variable["value"]?["type"]?.Value() ?? "object" }), enumerable = true, configurable = false, diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/JObjectValueCreator.cs b/src/mono/wasm/debugger/BrowserDebugProxy/JObjectValueCreator.cs index 523204d6ea858a..5b2eb76ff662b7 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/JObjectValueCreator.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/JObjectValueCreator.cs @@ -58,10 +58,10 @@ public static JObject Create(T value, return ret; } - public static JObject CreateFromPrimitiveType(object v) + public static JObject CreateFromPrimitiveType(object v, int? stringId = null) => v switch { - string s => Create(s, type: "string", description: s), + string s => Create(s, type: "string", description: s, objectId: $"dotnet:object:{stringId}"), char c => CreateJObjectForChar(Convert.ToInt32(c)), bool b => Create(b, type: "boolean", description: b ? "true" : "false", className: "System.Boolean"), @@ -182,7 +182,7 @@ public async Task ReadAsVariableValue( { var stringId = retDebuggerCmdReader.ReadInt32(); string value = await _sdbAgent.GetStringValue(stringId, token); - ret = CreateFromPrimitiveType(value); + ret = CreateFromPrimitiveType(value, stringId); break; } case ElementType.SzArray: diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs index 5a8226e0c7dc38..ec9477ab8f9bb3 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs @@ -327,8 +327,6 @@ async Task ResolveAsInstanceMember(ArraySegment parts, JObject if (!DotnetObjectId.TryParse(resolvedObject?["objectId"]?.Value(), out DotnetObjectId objectId)) { - if (resolvedObject["type"].Value() == "string") - throw new ReturnAsErrorException($"String properties evaluation is not supported yet.", "ReferenceError"); // Issue #66823 if (!throwOnNullReference) throw new ReturnAsErrorException($"Operation '?' not allowed on primitive type - '{parts[i - 1]}'", "ReferenceError"); throw new ReturnAsErrorException($"Cannot find member '{part}' on a primitive type", "ReferenceError"); @@ -358,7 +356,7 @@ async Task ResolveAsInstanceMember(ArraySegment parts, JObject } } - public async Task Resolve(ElementAccessExpressionSyntax elementAccess, Dictionary memberAccessValues, JObject indexObject, CancellationToken token) + public async Task Resolve(ElementAccessExpressionSyntax elementAccess, Dictionary memberAccessValues, JObject indexObject, List variableDefinitions, CancellationToken token) { try { @@ -374,6 +372,7 @@ public async Task Resolve(ElementAccessExpressionSyntax elementAccess, { string elementIdxStr; int elementIdx = 0; + var elementAccessStr = elementAccess.ToString(); // x[1] or x[a] or x[a.b] if (indexObject == null) { @@ -390,7 +389,7 @@ public async Task Resolve(ElementAccessExpressionSyntax elementAccess, } // e.g. x[a] or x[a.b] - if (arg.Expression is IdentifierNameSyntax) + else if (arg.Expression is IdentifierNameSyntax) { var argParm = arg.Expression as IdentifierNameSyntax; @@ -405,6 +404,8 @@ public async Task Resolve(ElementAccessExpressionSyntax elementAccess, elementIdxStr = indexObject["value"].ToString(); int.TryParse(elementIdxStr, out elementIdx); } + + // FixMe: indexing with expressions, e.g. x[a + 1] } } } @@ -416,22 +417,58 @@ public async Task Resolve(ElementAccessExpressionSyntax elementAccess, } if (elementIdx >= 0) { - DotnetObjectId.TryParse(rootObject?["objectId"]?.Value(), out DotnetObjectId objectId); + var type = rootObject?["type"]?.Value(); + if (!DotnetObjectId.TryParse(rootObject?["objectId"]?.Value(), out DotnetObjectId objectId)) + throw new InvalidOperationException($"Cannot apply indexing with [] to a primitive object of type '{type}'"); + switch (objectId.Scheme) { case "array": rootObject["value"] = await context.SdbAgent.GetArrayValues(objectId.Value, token); return (JObject)rootObject["value"][elementIdx]["value"]; case "object": + if (type == "string") + { + // ToArray() does not exist on string + var eaExpressionFormatted = elementAccessStrExpression.Replace('.', '_'); // instance_str + variableDefinitions.Add(ExpressionEvaluator.ConvertJSToCSharpLocalVariableAssignment(eaExpressionFormatted, rootObject)); + var eaFormatted = elementAccessStr.Replace('.', '_'); // instance_str[1] + return await ExpressionEvaluator.EvaluateSimpleExpression(this, eaFormatted, elementAccessStr, variableDefinitions, logger, token); + } var typeIds = await context.SdbAgent.GetTypeIdsForObject(objectId.Value, true, token); - int methodId = await context.SdbAgent.GetMethodIdByName(typeIds[0], "ToArray", token); - var toArrayRetMethod = await context.SdbAgent.InvokeMethod(objectId.Value, methodId, isValueType: false, token); - rootObject = await GetValueFromObject(toArrayRetMethod, token); - DotnetObjectId.TryParse(rootObject?["objectId"]?.Value(), out DotnetObjectId arrayObjectId); - rootObject["value"] = await context.SdbAgent.GetArrayValues(arrayObjectId.Value, token); - return (JObject)rootObject["value"][elementIdx]["value"]; + int[] methodIds = await context.SdbAgent.GetMethodIdsByName(typeIds[0], "ToArray", token); + // ToArray should not have an overload, but if user defined it, take the default one: without params + if (methodIds == null) + throw new InvalidOperationException($"Type '{rootObject?["className"]?.Value()}' cannot be indexed."); + + int toArrayId = methodIds[0]; + if (methodIds.Length > 1) + { + foreach (var methodId in methodIds) + { + MethodInfoWithDebugInformation methodInfo = await context.SdbAgent.GetMethodInfo(methodId, token); + ParameterInfo[] paramInfo = methodInfo.GetParametersInfo(); + if (paramInfo.Length == 0) + { + toArrayId = methodId; + break; + } + } + } + try + { + var toArrayRetMethod = await context.SdbAgent.InvokeMethod(objectId.Value, toArrayId, isValueType: false, token); + rootObject = await GetValueFromObject(toArrayRetMethod, token); + DotnetObjectId.TryParse(rootObject?["objectId"]?.Value(), out DotnetObjectId arrayObjectId); + rootObject["value"] = await context.SdbAgent.GetArrayValues(arrayObjectId.Value, token); + return (JObject)rootObject["value"][elementIdx]["value"]; + } + catch + { + throw new InvalidOperationException($"Cannot apply indexing with [] to an object of type '{rootObject?["className"]?.Value()}'"); + } default: - throw new InvalidOperationException($"Cannot apply indexing with [] to an expression of type '{objectId.Scheme}'"); + throw new InvalidOperationException($"Cannot apply indexing with [] to an expression of scheme '{objectId.Scheme}'"); } } } @@ -495,85 +532,100 @@ public async Task Resolve(InvocationExpressionSyntax method, Dictionary { if (!context.SdbAgent.ValueCreator.TryGetValueTypeById(objectId.Value, out ValueTypeClass valueType)) throw new Exception($"Could not find valuetype {objectId}"); - typeIds = new List(1) { valueType.TypeId }; } else { 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) + int[] methodIds = await context.SdbAgent.GetMethodIdsByName(typeIds[0], methodName, token); + if (methodIds == null) { - 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) - { - // instance method - commandParamsObjWriter.WriteObj(objectId, context.SdbAgent); - } - - 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) + //try to search on System.Linq.Enumerable + int methodId = await FindMethodIdOnLinqEnumerable(typeIds, methodName); + if (methodId == 0) { - // implicit *this* parameter - methodParamsCnt--; + var typeName = await context.SdbAgent.GetTypeName(typeIds[0], token); + throw new ReturnAsErrorException($"Method '{methodName}' not found in type '{typeName}'", "ReferenceError"); } - if (passedArgsCnt > methodParamsCnt) - throw new ReturnAsErrorException($"Unable to evaluate method '{methodName}'. Too many arguments passed.", "ArgumentError"); + methodIds = new int[] { methodId }; } - + // get information about params in all overloads for *methodName* + List methodInfos = await GetMethodParamInfosForMethods(methodIds); + int passedArgsCnt = method.ArgumentList.Arguments.Count; + int maxMethodParamsCnt = methodInfos.Max(v => v.GetParametersInfo().Length); if (isExtensionMethod) { - commandParamsObjWriter.Write(methodParamsCnt + 1); - commandParamsObjWriter.WriteObj(objectId, context.SdbAgent); - } - else - { - commandParamsObjWriter.Write(methodParamsCnt); + // implicit *this* parameter + maxMethodParamsCnt--; } + if (passedArgsCnt > maxMethodParamsCnt) + throw new ReturnAsErrorException($"Unable to evaluate method '{methodName}'. Too many arguments passed.", "ArgumentError"); - int argIndex = 0; - // explicitly passed arguments - for (; argIndex < passedArgsCnt; argIndex++) + foreach (var methodInfo in methodInfos) { - var arg = method.ArgumentList.Arguments[argIndex]; - if (arg.Expression is LiteralExpressionSyntax literal) + ParameterInfo[] methodParamsInfo = methodInfo.GetParametersInfo(); + int methodParamsCnt = isExtensionMethod ? methodParamsInfo.Length - 1 : methodParamsInfo.Length; + int optionalParams = methodParamsInfo.Count(v => v.Value != null); + if (passedArgsCnt > methodParamsCnt || passedArgsCnt < methodParamsCnt - optionalParams) { - if (!await commandParamsObjWriter.WriteConst(literal, context.SdbAgent, token)) - throw new InternalErrorException($"Unable to evaluate method '{methodName}'. Unable to write LiteralExpressionSyntax into binary writer."); + // this overload does not match the number of params passed, try another one + continue; } - else if (arg.Expression is IdentifierNameSyntax identifierName) + int methodId = methodInfo.DebugId; + using var commandParamsObjWriter = new MonoBinaryWriter(); + + if (isExtensionMethod) { - 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."); + commandParamsObjWriter.Write(methodParamsCnt + 1); + commandParamsObjWriter.WriteObj(objectId, context.SdbAgent); } else { - throw new InternalErrorException($"Unable to evaluate method '{methodName}'. Unable to write into binary writer, not recognized expression type: {arg.Expression.GetType().Name}"); + // instance method + commandParamsObjWriter.WriteObj(objectId, context.SdbAgent); + 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 evaluate method '{methodName}'. Unable to write LiteralExpressionSyntax 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 evaluate method '{methodName}'. Unable to write IdentifierNameSyntax into binary writer."); + } + else + { + throw new InternalErrorException($"Unable to evaluate method '{methodName}'. 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."); + } + try + { + var retMethod = await context.SdbAgent.InvokeMethod(commandParamsObjWriter.GetParameterBuffer(), methodId, token); + return await GetValueFromObject(retMethod, token); + } + catch + { + // try further methodIds, we're looking for a method with the same type of params that the user passed + logger.LogDebug($"InvokeMethod failed due to parameter type mismatch for {methodName} with {methodParamsCnt} parameters, including {optionalParams} optional."); + continue; } } - // 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 ReturnAsErrorException($"No implementation of method '{methodName}' matching '{method}' found in type {rootObject["className"]}.", "ArgumentError"); } catch (Exception ex) when (ex is not (ExpressionEvaluationFailedException or ReturnAsErrorException)) { @@ -592,8 +644,8 @@ async Task FindMethodIdOnLinqEnumerable(IList typeIds, string methodNa } } - int newMethodId = await context.SdbAgent.GetMethodIdByName(linqTypeId, methodName, token); - if (newMethodId == 0) + int[] newMethodIds = await context.SdbAgent.GetMethodIdsByName(linqTypeId, methodName, token); + if (newMethodIds == null) return 0; foreach (int typeId in typeIds) @@ -602,36 +654,34 @@ async Task FindMethodIdOnLinqEnumerable(IList typeIds, string methodNa if (genericTypeArgs.Count > 0) { isExtensionMethod = true; - return await context.SdbAgent.MakeGenericMethod(newMethodId, genericTypeArgs, token); + return await context.SdbAgent.MakeGenericMethod(newMethodIds[0], genericTypeArgs, token); } } return 0; } + + async Task> GetMethodParamInfosForMethods(int[] methodIds) + { + List allMethodInfos = new(); + for (int i = 0; i < methodIds.Length; i++) + { + var ithMethodInfo = await context.SdbAgent.GetMethodInfo(methodIds[i], token); + if (ithMethodInfo != null) + allMethodInfos.Add(ithMethodInfo); + } + return allMethodInfos; + } } - private static readonly HashSet NumericTypes = new HashSet + public JObject ConvertCSharpToJSType(object v, Type type) { - typeof(decimal), typeof(byte), typeof(sbyte), - typeof(short), typeof(ushort), - typeof(int), typeof(uint), - typeof(float), typeof(double) - }; + if (v is JObject jobj) + return jobj; + + if (v is null) + return JObjectValueCreator.CreateNull("")?["value"] as JObject; - 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( @@ -650,7 +700,15 @@ public object ConvertCSharpToJSType(object v, Type type) className = type.ToString() })); } - return new { type = "object", value = v, description = v.ToString(), className = type.ToString() }; + + string typeName = v.GetType().ToString(); + jobj = JObjectValueCreator.CreateFromPrimitiveType(v); + return jobj is not null + ? jobj["value"] as JObject + : JObjectValueCreator.Create(value: null, + type: "object", + description: v.ToString(), + className: typeName)?["value"] as JObject; } private JObject CacheEvaluationResult(JObject value) diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs index 83fecee748bf9c..ba0235f25749d8 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs @@ -404,7 +404,27 @@ internal string GetArrayIndexString(int idx) } } - internal sealed record MethodInfoWithDebugInformation(MethodInfo Info, int DebugId, string Name); + internal sealed class MethodInfoWithDebugInformation + { + private ParameterInfo[] _paramsInfo; + public MethodInfo Info { get; } + public int DebugId { get; } + public string Name { get; } + public ParameterInfo[] GetParametersInfo() + { + if (_paramsInfo != null) + return _paramsInfo; + _paramsInfo = Info.GetParametersInfo(); + return _paramsInfo; + } + + public MethodInfoWithDebugInformation(MethodInfo info, int debugId, string name) + { + Info = info; + DebugId = debugId; + Name = name; + } + } internal sealed class TypeInfoWithDebugInformation { @@ -1712,7 +1732,7 @@ public async Task GetTypeIdFromToken(int assemblyId, int typeToken, Cancell return retDebuggerCmdReader.ReadInt32(); } - public async Task GetMethodIdByName(int type_id, string method_name, CancellationToken token) + public async Task GetMethodIdsByName(int type_id, string method_name, CancellationToken token) { if (type_id <= 0) throw new DebuggerAgentException($"Invalid type_id {type_id} (method_name: {method_name}"); @@ -1724,7 +1744,12 @@ public async Task GetMethodIdByName(int type_id, string method_name, Cancel commandParamsWriter.Write((int)1); //case sensitive using var retDebuggerCmdReader = await SendDebuggerAgentCommand(CmdType.GetMethodsByNameFlags, commandParamsWriter, token); var nMethods = retDebuggerCmdReader.ReadInt32(); - return retDebuggerCmdReader.ReadInt32(); + if (nMethods == 0) + return null; + int[] methodIds = new int[nMethods]; + for (int i = 0; i < nMethods; i++) + methodIds[i] = retDebuggerCmdReader.ReadInt32(); + return methodIds; } public async Task IsDelegate(int objectId, CancellationToken token) @@ -2136,7 +2161,9 @@ private async Task FindDebuggerProxyConstructorIdFor(int typeId, Cancellati break; cAttrTypeId = genericTypeId; } - methodId = await GetMethodIdByName(cAttrTypeId, ".ctor", token); + int[] methodIds = await GetMethodIdsByName(cAttrTypeId, ".ctor", token); + if (methodIds != null) + methodId = methodIds[0]; break; } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs b/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs index 6c781acc27c35c..3eadb5b7517d4d 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs @@ -102,8 +102,10 @@ public async Task ToJObject(MonoSDBHelper sdbAgent, bool forDebuggerDis string description = className; if (ShouldAutoInvokeToString(className) || IsEnum) { - int methodId = await sdbAgent.GetMethodIdByName(TypeId, "ToString", token); - var retMethod = await sdbAgent.InvokeMethod(Buffer, methodId, token, "methodRet"); + int[] methodIds = await sdbAgent.GetMethodIdsByName(TypeId, "ToString", token); + if (methodIds == null) + throw new InternalErrorException($"Cannot find method 'ToString' on typeId = {TypeId}"); + var retMethod = await sdbAgent.InvokeMethod(Buffer, methodIds[0], token, "methodRet"); description = retMethod["value"]?["value"].Value(); if (className.Equals("System.Guid")) description = description.ToUpperInvariant(); //to keep the old behavior diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestFirefox.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestFirefox.cs index 2bc02b88f483c8..a3d28ed571d347 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestFirefox.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestFirefox.cs @@ -226,52 +226,63 @@ internal JObject ConvertFromFirefoxToDefaultFormat(KeyValuePair JToken value = variable.Value; JObject variableValue = null; string valueType = "value"; - if (value?["type"] == null || value["type"].Value() == "object") + if (value?["type"] == null || value["type"].Value() == "object" || value["type"].Value() == "string") { var actor = value["value"]?["actor"]?.Value(); - if (value["value"]["type"].Value() == "null") + string type = value["value"]?["type"]?.Value(); + switch (type) { - variableValue = JObject.FromObject(new + case "null": + variableValue = JObject.FromObject(new { type = "object", subtype = "null", className = value["value"]["class"].Value(), description = value["value"]["class"].Value() }); - if (actor != null && actor.StartsWith("dotnet:pointer:")) - variableValue["type"] = "symbol"; - } - else if (value?["value"]?["type"].Value() == "function") - { - variableValue = JObject.FromObject(new - { - type = "function", - objectId = value["value"]["actor"].Value(), - className = "Function", - description = $"get {name} ()" - }); - valueType = "get"; - } - else { - variableValue = JObject.FromObject(new + if (actor != null && actor.StartsWith("dotnet:pointer:")) + variableValue["type"] = "symbol"; + break; + case "function": + variableValue = JObject.FromObject(new { - type = value["value"]["type"], + type = type, + objectId = value["value"]["actor"].Value(), + className = "Function", + description = $"get {name} ()" + }); + valueType = "get"; + break; + case "string": + variableValue = JObject.FromObject(new + { + type = type, + objectId = value["value"]["actor"]?.Value(), + value = value["value"]["value"]?.Value(), + description = value["value"]["value"].Value() + }); + break; + default: + variableValue = JObject.FromObject(new + { + type = type, value = (string)null, description = value["value"]?["value"]?.Value() == null ? value["value"]["class"].Value() : value["value"]?["value"]?.Value(), className = value["value"]["class"].Value(), objectId = actor, }); - if (actor.StartsWith("dotnet:valuetype:")) - variableValue["isValueType"] = true; - if (actor.StartsWith("dotnet:array:")) - variableValue["subtype"] = "array"; - if (actor.StartsWith("dotnet:pointer:")) - variableValue["type"] = "object"; - if (actor.StartsWith("dotnet:pointer:-1")) - { - variableValue["type"] = "symbol"; - variableValue["value"] = value["value"]?["value"]?.Value(); - } + if (actor.StartsWith("dotnet:valuetype:")) + variableValue["isValueType"] = true; + if (actor.StartsWith("dotnet:array:")) + variableValue["subtype"] = "array"; + if (actor.StartsWith("dotnet:pointer:")) + variableValue["type"] = "object"; + if (actor.StartsWith("dotnet:pointer:-1")) + { + variableValue["type"] = "symbol"; + variableValue["value"] = value["value"]?["value"]?.Value(); + } + break; } } else diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs index 800c17b45843d0..60fea60f9af1ff 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs @@ -514,15 +514,13 @@ public async Task EvaluateSimpleMethodCallsError() => await CheckInspectLocalsAt 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()); + Assert.Contains("No implementation of method 'CallMethodWithParm' matching 'this.CallMethodWithParm(\"1\")' found in type DebuggerTests.EvaluateMethodTestsClass.TestEvaluate.", res.Error["result"]?["description"]?.Value()); (_, res) = await EvaluateOnCallFrame(id, "this.ParmToTestObjNull.MyMethod()", expect_ok: false ); 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 evaluate '(this.ParmToTestObjException.MyMethod()\n)'", - res.Error["result"]?["description"]?.Value()); + Assert.Equal("Method 'MyMethod' not found in type 'string'", res.Error["result"]?["description"]?.Value()); }); [Fact] @@ -585,10 +583,23 @@ await EvaluateOnCallFrameAndCheck(id, ("this.CallMethodWithObj(this.objToTest)", TNumber(10))); }); + [ConditionalFact(nameof(RunningOnChrome))] + public async Task EvaluateIndexingNegative() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateLocalsWithIndexingTests", "EvaluateLocals", 5, "DebuggerTests.EvaluateLocalsWithIndexingTests.EvaluateLocals", + $"window.setTimeout(function() {{ invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithIndexingTests:EvaluateLocals'); 1 }})", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + var (_, res) = await EvaluateOnCallFrame(id, "f.idx0[2]", expect_ok: false ); + Assert.Equal("Unable to evaluate element access 'f.idx0[2]': Cannot apply indexing with [] to a primitive object of type 'number'", res.Error["message"]?.Value()); + (_, res) = await EvaluateOnCallFrame(id, "f[1]", expect_ok: false ); + Assert.Equal( "Unable to evaluate element access 'f[1]': Type 'DebuggerTests.EvaluateLocalsWithIndexingTests.TestEvaluate' cannot be indexed.", res.Error["message"]?.Value()); + }); + [Fact] - public async Task EvaluateExpressionsWithElementAccessByConstant() => await CheckInspectLocalsAtBreakpointSite( - "DebuggerTests.EvaluateLocalsWithElementAccessTests", "EvaluateLocals", 5, "DebuggerTests.EvaluateLocalsWithElementAccessTests.EvaluateLocals", - "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithElementAccessTests:EvaluateLocals'); })", + public async Task EvaluateIndexingsByConstant() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateLocalsWithIndexingTests", "EvaluateLocals", 5, "DebuggerTests.EvaluateLocalsWithIndexingTests.EvaluateLocals", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithIndexingTests:EvaluateLocals'); })", wait_for_event_fn: async (pause_location) => { var id = pause_location["callFrames"][0]["callFrameId"].Value(); @@ -601,9 +612,9 @@ await EvaluateOnCallFrameAndCheck(id, }); [Fact] - public async Task EvaluateExpressionsWithElementAccessByLocalVariable() => await CheckInspectLocalsAtBreakpointSite( - "DebuggerTests.EvaluateLocalsWithElementAccessTests", "EvaluateLocals", 5, "DebuggerTests.EvaluateLocalsWithElementAccessTests.EvaluateLocals", - "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithElementAccessTests:EvaluateLocals'); })", + public async Task EvaluateIndexingByLocalVariable() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateLocalsWithIndexingTests", "EvaluateLocals", 5, "DebuggerTests.EvaluateLocalsWithIndexingTests.EvaluateLocals", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithIndexingTests:EvaluateLocals'); })", wait_for_event_fn: async (pause_location) => { var id = pause_location["callFrames"][0]["callFrameId"].Value(); @@ -617,9 +628,9 @@ await EvaluateOnCallFrameAndCheck(id, }); [Fact] - public async Task EvaluateExpressionsWithElementAccessByMemberVariables() => await CheckInspectLocalsAtBreakpointSite( - "DebuggerTests.EvaluateLocalsWithElementAccessTests", "EvaluateLocals", 5, "DebuggerTests.EvaluateLocalsWithElementAccessTests.EvaluateLocals", - "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithElementAccessTests:EvaluateLocals'); })", + public async Task EvaluateIndexingByMemberVariables() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateLocalsWithIndexingTests", "EvaluateLocals", 5, "DebuggerTests.EvaluateLocalsWithIndexingTests.EvaluateLocals", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithIndexingTests:EvaluateLocals'); })", wait_for_event_fn: async (pause_location) => { var id = pause_location["callFrames"][0]["callFrameId"].Value(); @@ -635,9 +646,9 @@ await EvaluateOnCallFrameAndCheck(id, }); [Fact] - public async Task EvaluateExpressionsWithElementAccessNested() => await CheckInspectLocalsAtBreakpointSite( - "DebuggerTests.EvaluateLocalsWithElementAccessTests", "EvaluateLocals", 5, "DebuggerTests.EvaluateLocalsWithElementAccessTests.EvaluateLocals", - "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithElementAccessTests:EvaluateLocals'); })", + public async Task EvaluateIndexingNested() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateLocalsWithIndexingTests", "EvaluateLocals", 5, "DebuggerTests.EvaluateLocalsWithIndexingTests.EvaluateLocals", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithIndexingTests:EvaluateLocals'); })", wait_for_event_fn: async (pause_location) => { var id = pause_location["callFrames"][0]["callFrameId"].Value(); @@ -652,9 +663,9 @@ await EvaluateOnCallFrameAndCheck(id, }); [ConditionalFact(nameof(RunningOnChrome))] - public async Task EvaluateExpressionsWithElementAccessMultidimentional() => await CheckInspectLocalsAtBreakpointSite( - "DebuggerTests.EvaluateLocalsWithElementAccessTests", "EvaluateLocals", 5, "DebuggerTests.EvaluateLocalsWithElementAccessTests.EvaluateLocals", - "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithElementAccessTests:EvaluateLocals'); })", + public async Task EvaluateIndexingMultidimensional() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateLocalsWithIndexingTests", "EvaluateLocals", 5, "DebuggerTests.EvaluateLocalsWithIndexingTests.EvaluateLocals", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithIndexingTests:EvaluateLocals'); })", wait_for_event_fn: async (pause_location) => { var id = pause_location["callFrames"][0]["callFrameId"].Value(); @@ -1181,7 +1192,7 @@ await EvaluateOnCallFrameAndCheck(id, [ConditionalFact(nameof(RunningOnChrome))] public async Task EvaluateNullObjectPropertiesPositive() => await CheckInspectLocalsAtBreakpointSite( - $"DebuggerTests.EvaluateNullableProperties", "Evaluate", 6, "DebuggerTests.EvaluateNullableProperties.Evaluate", + $"DebuggerTests.EvaluateNullableProperties", "Evaluate", 11, "DebuggerTests.EvaluateNullableProperties.Evaluate", $"window.setTimeout(function() {{ invoke_static_method ('[debugger-test] DebuggerTests.EvaluateNullableProperties:Evaluate'); 1 }})", wait_for_event_fn: async (pause_location) => { @@ -1200,7 +1211,11 @@ await EvaluateOnCallFrameAndCheck(id, ("tc!.MemberList!.Count", TNumber(2)), ("tc?.MemberListNull?.Count", TObject("System.Collections.Generic.List", is_null: true)), ("tc.MemberListNull?.Count", TObject("System.Collections.Generic.List", is_null: true)), - ("tcNull?.MemberListNull?.Count", TObject("DebuggerTests.EvaluateNullableProperties.TestClass", is_null: true))); + ("tcNull?.MemberListNull?.Count", TObject("DebuggerTests.EvaluateNullableProperties.TestClass", is_null: true)), + ("str!.Length", TNumber(9)), + ("str?.Length", TNumber(9)), + ("str_null?.Length", TObject("string", is_null: true)) + ); }); [Fact] @@ -1226,8 +1241,11 @@ await CheckEvaluateFail(id, ("tcNull?.Sibling.MemberListNull?.Count", GetNullReferenceErrorOn("\"MemberListNull?\"")), ("listNull?", "Expected expression."), ("listNull!.Count", GetNullReferenceErrorOn("\"Count\"")), - ("x?.p", "Operation '?' not allowed on primitive type - 'x?'") + ("x?.p", "Operation '?' not allowed on primitive type - 'x?'"), + ("str_null.Length", GetNullReferenceErrorOn("\"Length\"")), + ("str_null!.Length", GetNullReferenceErrorOn("\"Length\"")) ); + string GetNullReferenceErrorOn(string name) => $"Expression threw NullReferenceException trying to access {name} on a null-valued object."; }); @@ -1246,7 +1264,6 @@ await EvaluateOnCallFrameAndCheck(id, ("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")), @@ -1258,9 +1275,9 @@ await EvaluateOnCallFrameAndCheck(id, ("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))); + ("localString.StartsWith('S')", TBool(true)) + ); }); [Fact] @@ -1295,15 +1312,15 @@ public async Task EvaluateMethodsOnPrimitiveTypesReturningObjects() => await Ch 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") }; + var props = await GetProperties(res["objectId"]?.Value()); + 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") }; + props = await GetProperties(res["objectId"]?.Value()); + expected_props = new[] { TString("S"), TString("T"), TString("R") }; await CheckProps(props, expected_props, "props#2"); - }); + }); [Theory] [InlineData("DefaultMethod", "IDefaultInterface", "Evaluate")] @@ -1338,5 +1355,20 @@ await EvaluateOnCallFrameAndCheck(id, ("defaultInterfaceMember", TString("defaultInterfaceMember")) ); }); + + [Fact] + public async Task EvaluateStringProperties() => await CheckInspectLocalsAtBreakpointSite( + $"DebuggerTests.TypeProperties", "Run", 3, "DebuggerTests.TypeProperties.Run", + $"window.setTimeout(function() {{ invoke_static_method ('[debugger-test] DebuggerTests.TypeProperties:Run'); 1 }})", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + await EvaluateOnCallFrameAndCheck(id, + ("localString.Length", TNumber(5)), + ("localString[1]", TChar('B')), + ("instance.str.Length", TNumber(5)), + ("instance.str[3]", TChar('c')) + ); + }); } } 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 74a8ac7424c460..27af8156168fe2 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 @@ -504,7 +504,7 @@ public async void run() } } - public class EvaluateLocalsWithElementAccessTests + public class EvaluateLocalsWithIndexingTests { public class TestEvaluate { @@ -1459,6 +1459,20 @@ static void Evaluate() int? x_val = x; } } + + public static class TypeProperties + { + public class InstanceProperties + { + public string str = "aB.c["; + } + + public static void Run() + { + var instance = new InstanceProperties(); + var localString = "aB.c["; + } + } } namespace DebuggerTestsV2