diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs index a08a62512de9ef..9a5761f60c325a 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs @@ -193,6 +193,12 @@ public static MonoCommands GetScopeVariables(int scopeId, params VarInfo[] vars) return new MonoCommands($"MONO.mono_wasm_get_variables({scopeId}, {JsonConvert.SerializeObject(var_ids)})"); } + public static MonoCommands EvaluateMemberAccess(int scopeId, string expr, params VarInfo[] vars) + { + var var_ids = vars.Select(v => new { index = v.Index, name = v.Name }).ToArray(); + return new MonoCommands($"MONO.mono_wasm_eval_member_access({scopeId}, {JsonConvert.SerializeObject(var_ids)}, '', '{expr}')"); + } + public static MonoCommands SetBreakpoint(string assemblyName, uint methodToken, int ilOffset) => new MonoCommands($"MONO.mono_wasm_set_breakpoint (\"{assemblyName}\", {methodToken}, {ilOffset})"); public static MonoCommands RemoveBreakpoint(int breakpointId) => new MonoCommands($"MONO.mono_wasm_remove_breakpoint({breakpointId})"); @@ -285,7 +291,7 @@ internal class ExecutionContext internal DebugStore store; public TaskCompletionSource Source { get; } = new TaskCompletionSource(); - public Dictionary LocalsCache = new Dictionary(); + Dictionary perScopeCaches { get; } = new Dictionary(); public DebugStore Store { @@ -298,11 +304,26 @@ public DebugStore Store } } + public PerScopeCache GetCacheForScope(int scope_id) + { + if (perScopeCaches.TryGetValue(scope_id, out var cache)) + return cache; + + cache = new PerScopeCache(); + perScopeCaches[scope_id] = cache; + return cache; + } + public void ClearState() { CallStack = null; - LocalsCache.Clear(); + perScopeCaches.Clear(); } + } + internal class PerScopeCache + { + public Dictionary Locals { get; } = new Dictionary(); + public Dictionary MemberReferences { get; } = new Dictionary(); } } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs index 9f34078b796dc5..8d43bda0243c55 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs @@ -6,12 +6,14 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Emit; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Microsoft.WebAssembly.Diagnostics @@ -19,54 +21,30 @@ namespace Microsoft.WebAssembly.Diagnostics internal class EvaluateExpression { - - class FindThisExpression : CSharpSyntaxWalker + class FindVariableNMethodCall : CSharpSyntaxWalker { - public List thisExpressions = new List(); - public SyntaxTree syntaxTree; - public FindThisExpression(SyntaxTree syntax) - { - syntaxTree = syntax; - } + public List identifiers = new List(); + public List methodCall = new List(); + public List memberAccesses = new List(); + public List argValues = new List(); + public override void Visit(SyntaxNode node) { - if (node is ThisExpressionSyntax) + // TODO: PointerMemberAccessExpression + if (node is MemberAccessExpressionSyntax maes + && node.Kind() == SyntaxKind.SimpleMemberAccessExpression + && !(node.Parent is MemberAccessExpressionSyntax)) { - if (node.Parent is MemberAccessExpressionSyntax thisParent && thisParent.Name is IdentifierNameSyntax) - { - IdentifierNameSyntax var = thisParent.Name as IdentifierNameSyntax; - thisExpressions.Add(var.Identifier.Text); - var newRoot = syntaxTree.GetRoot().ReplaceNode(node.Parent, thisParent.Name); - syntaxTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options); - this.Visit(GetExpressionFromSyntaxTree(syntaxTree)); - } + memberAccesses.Add(maes); } - else - base.Visit(node); - } - public async Task CheckIfIsProperty(MonoProxy proxy, MessageId msg_id, int scope_id, CancellationToken token) - { - foreach (var var in thisExpressions) + if (node is IdentifierNameSyntax identifier + && !(identifier.Parent is MemberAccessExpressionSyntax) + && !identifiers.Any(x => x.Identifier.Text == identifier.Identifier.Text)) { - JToken value = await proxy.TryGetVariableValue(msg_id, scope_id, var, true, token); - if (value == null) - throw new Exception($"The property {var} does not exist in the current context"); + identifiers.Add(identifier); } - } - } - - class FindVariableNMethodCall : CSharpSyntaxWalker - { - public List variables = new List(); - public List thisList = new List(); - public List methodCall = new List(); - public List values = new List(); - public override void Visit(SyntaxNode node) - { - if (node is IdentifierNameSyntax identifier && !variables.Any(x => x.Identifier.Text == identifier.Identifier.Text)) - variables.Add(identifier); if (node is InvocationExpressionSyntax) { methodCall.Add(node as InvocationExpressionSyntax); @@ -76,29 +54,74 @@ public override void Visit(SyntaxNode node) throw new Exception("Assignment is not implemented yet"); base.Visit(node); } - public async Task ReplaceVars(SyntaxTree syntaxTree, MonoProxy proxy, MessageId msg_id, int scope_id, CancellationToken token) + + public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable ma_values, IEnumerable id_values) { CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot(); - foreach (var var in variables) + var memberAccessToParamName = new Dictionary(); + + // 1. Replace all this.a occurrences with this_a_ABDE + root = root.ReplaceNodes(memberAccesses, (maes, _) => + { + var ma_str = maes.ToString(); + if (!memberAccessToParamName.TryGetValue(ma_str, out var id_name)) + { + // Generate a random suffix + string suffix = Guid.NewGuid().ToString().Substring(0, 5); + string prefix = ma_str.Trim().Replace(".", "_"); + id_name = $"{prefix}_{suffix}"; + + memberAccessToParamName[ma_str] = id_name; + } + + return SyntaxFactory.IdentifierName(id_name); + }); + + var paramsSet = new HashSet(); + + // 2. For every unique member ref, add a corresponding method param + foreach (var (maes, value) in memberAccesses.Zip(ma_values)) { - ClassDeclarationSyntax classDeclaration = root.Members.ElementAt(0) as ClassDeclarationSyntax; - MethodDeclarationSyntax method = classDeclaration.Members.ElementAt(0) as MethodDeclarationSyntax; + var node_str = maes.ToString(); + if (!memberAccessToParamName.TryGetValue(node_str, out var id_name)) + { + throw new Exception($"BUG: Expected to find an id name for the member access string: {node_str}"); + } - JToken value = await proxy.TryGetVariableValue(msg_id, scope_id, var.Identifier.Text, false, token); + root = UpdateWithNewMethodParam(root, id_name, value); + } - if (value == null) - throw new Exception($"The name {var.Identifier.Text} does not exist in the current context"); + foreach (var (idns, value) in identifiers.Zip(id_values)) + { + root = UpdateWithNewMethodParam(root, idns.Identifier.Text, value); + } - values.Add(ConvertJSToCSharpType(value["value"])); + return syntaxTree.WithRootAndOptions(root, syntaxTree.Options); + + CompilationUnitSyntax UpdateWithNewMethodParam(CompilationUnitSyntax root, string id_name, JObject value) + { + var classDeclaration = root.Members.ElementAt(0) as ClassDeclarationSyntax; + var method = classDeclaration.Members.ElementAt(0) as MethodDeclarationSyntax; + + if (paramsSet.Contains(id_name)) + { + // repeated member access expression + // eg. this.a + this.a + return root; + } + + argValues.Add(ConvertJSToCSharpType(value)); var updatedMethod = method.AddParameterListParameters( SyntaxFactory.Parameter( - SyntaxFactory.Identifier(var.Identifier.Text)) - .WithType(SyntaxFactory.ParseTypeName(GetTypeFullName(value["value"])))); + SyntaxFactory.Identifier(id_name)) + .WithType(SyntaxFactory.ParseTypeName(GetTypeFullName(value)))); + + paramsSet.Add(id_name); root = root.ReplaceNode(method, updatedMethod); + + return root; } - syntaxTree = syntaxTree.WithRootAndOptions(root, syntaxTree.Options); - return syntaxTree; } private object ConvertJSToCSharpType(JToken variable) @@ -120,7 +143,7 @@ private object ConvertJSToCSharpType(JToken variable) return null; break; } - throw new Exception($"Evaluate of this datatype {type} not implemented yet"); + throw new Exception($"Evaluate of this datatype {type} not implemented yet");//, "Unsupported"); } private string GetTypeFullName(JToken variable) @@ -140,7 +163,7 @@ private string GetTypeFullName(JToken variable) default: return value.GetType().FullName; } - throw new Exception($"Evaluate of this datatype {type} not implemented yet"); + throw new ReturnAsErrorException($"GetTypefullName: Evaluate of this datatype {type} not implemented yet", "Unsupported"); } } @@ -151,36 +174,89 @@ static SyntaxNode GetExpressionFromSyntaxTree(SyntaxTree syntaxTree) MethodDeclarationSyntax methodDeclaration = classDeclaration.Members.ElementAt(0) as MethodDeclarationSyntax; BlockSyntax blockValue = methodDeclaration.Body; ReturnStatementSyntax returnValue = blockValue.Statements.ElementAt(0) as ReturnStatementSyntax; - InvocationExpressionSyntax expressionInvocation = returnValue.Expression as InvocationExpressionSyntax; - MemberAccessExpressionSyntax expressionMember = expressionInvocation.Expression as MemberAccessExpressionSyntax; - ParenthesizedExpressionSyntax expressionParenthesized = expressionMember.Expression as ParenthesizedExpressionSyntax; - return expressionParenthesized.Expression; + ParenthesizedExpressionSyntax expressionParenthesized = returnValue.Expression as ParenthesizedExpressionSyntax; + + return expressionParenthesized?.Expression; } - internal static async Task CompileAndRunTheExpression(MonoProxy proxy, MessageId msg_id, int scope_id, string expression, CancellationToken token) + private static async Task> ResolveMemberAccessExpressions(IEnumerable member_accesses, + MemberReferenceResolver resolver, CancellationToken token) + { + var memberAccessValues = new List(); + foreach (var maes in member_accesses) + { + var memberAccessString = maes.ToString(); + var 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; + } + + private static async Task> ResolveIdentifiers(IEnumerable identifiers, MemberReferenceResolver resolver, CancellationToken token) + { + var values = new List(); + foreach (var 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); + } + + return values; + } + + internal static async Task CompileAndRunTheExpression(string expression, MemberReferenceResolver resolver, CancellationToken token) { - FindVariableNMethodCall findVarNMethodCall = new FindVariableNMethodCall(); - string retString; SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@" - using System; - public class CompileAndRunTheExpression - { - public string Evaluate() - { - return (" + expression + @").ToString(); - } - }"); - - FindThisExpression findThisExpression = new FindThisExpression(syntaxTree); + using System; + public class CompileAndRunTheExpression + { + public static object Evaluate() + { + return (" + expression + @"); + } + }"); + var expressionTree = GetExpressionFromSyntaxTree(syntaxTree); - findThisExpression.Visit(expressionTree); - await findThisExpression.CheckIfIsProperty(proxy, msg_id, scope_id, token); - syntaxTree = findThisExpression.syntaxTree; + if (expressionTree == null) + throw new Exception($"BUG: Unable to evaluate {expression}, could not get expression from the syntax tree"); - expressionTree = GetExpressionFromSyntaxTree(syntaxTree); + FindVariableNMethodCall findVarNMethodCall = new FindVariableNMethodCall(); findVarNMethodCall.Visit(expressionTree); - syntaxTree = await findVarNMethodCall.ReplaceVars(syntaxTree, proxy, msg_id, scope_id, token); + // this fails with `"a)"` + // because the code becomes: return (a)); + // and the returned expression from GetExpressionFromSyntaxTree is `a`! + if (expressionTree.Kind() == SyntaxKind.IdentifierName || expressionTree.Kind() == SyntaxKind.ThisExpression) + { + var var_name = expressionTree.ToString(); + var value = await resolver.Resolve(var_name, token); + if (value == null) + throw new ReturnAsErrorException($"Cannot find member named '{var_name}'.", "ReferenceError"); + + return value; + } + + var memberAccessValues = await ResolveMemberAccessExpressions(findVarNMethodCall.memberAccesses, resolver, token); + + // this.dateTime + if (expressionTree.Kind() == SyntaxKind.SimpleMemberAccessExpression && findVarNMethodCall.memberAccesses.Count == 1) + { + return memberAccessValues[0]?["value"]?.Value(); + } + + var identifierValues = await ResolveIdentifiers(findVarNMethodCall.identifiers, resolver, token); + + syntaxTree = findVarNMethodCall.ReplaceVars(syntaxTree, memberAccessValues, identifierValues); + expressionTree = GetExpressionFromSyntaxTree(syntaxTree); + if (expressionTree == null) + throw new Exception($"BUG: Unable to evaluate {expression}, could not get expression from the syntax tree"); MetadataReference[] references = new MetadataReference[] { @@ -193,21 +269,83 @@ public string Evaluate() syntaxTrees: new[] { syntaxTree }, references: references, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var semanticModel = compilation.GetSemanticModel(syntaxTree); + var typeInfo = semanticModel.GetTypeInfo(expressionTree); + using (var ms = new MemoryStream()) { EmitResult result = compilation.Emit(ms); + if (!result.Success) + { + var sb = new StringBuilder(); + foreach (var d in result.Diagnostics) + sb.Append(d.ToString()); + + throw new ReturnAsErrorException(sb.ToString(), "CompilationError"); + } + ms.Seek(0, SeekOrigin.Begin); Assembly assembly = Assembly.Load(ms.ToArray()); Type type = assembly.GetType("CompileAndRunTheExpression"); - object obj = Activator.CreateInstance(type); + var ret = type.InvokeMember("Evaluate", - BindingFlags.Default | BindingFlags.InvokeMethod, + BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, + null, null, - obj, - findVarNMethodCall.values.ToArray()); - retString = ret.ToString(); + findVarNMethodCall.argValues.ToArray()); + + return JObject.FromObject(ConvertCSharpToJSType(ret, typeInfo.Type)); + } + } + + static readonly HashSet NumericTypes = new HashSet + { + typeof(decimal), typeof(byte), typeof(sbyte), + typeof(short), typeof(ushort), + typeof(int), typeof(uint), + typeof(float), typeof(double) + }; + + static object ConvertCSharpToJSType(object v, ITypeSymbol type) + { + if (v == null) + return new { type = "object", subtype = "null", className = type.ToString() }; + + if (v is string s) + { + return new { type = "string", value = s, description = s }; + } + else if (NumericTypes.Contains(v.GetType())) + { + return new { type = "number", value = v, description = v.ToString() }; } - return retString; + else + { + return new { type = "object", value = v, description = v.ToString(), className = type.ToString() }; + } + } + + } + + class ReturnAsErrorException : Exception + { + public Result Error { get; } + public ReturnAsErrorException(JObject error) + => Error = Result.Err(error); + + public ReturnAsErrorException(string message, string className) + { + Error = Result.Err(JObject.FromObject(new + { + result = new + { + type = "object", + subtype = "error", + description = message, + className + } + })); } } } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs new file mode 100644 index 00000000000000..145e2237c5e2ab --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.WebAssembly.Diagnostics +{ + internal class MemberReferenceResolver + { + private MessageId messageId; + private int scopeId; + private MonoProxy proxy; + private ExecutionContext ctx; + private PerScopeCache scopeCache; + private VarInfo[] varIds; + private ILogger logger; + private bool locals_fetched = false; + + public MemberReferenceResolver(MonoProxy proxy, ExecutionContext ctx, MessageId msg_id, int scope_id, ILogger logger) + { + messageId = msg_id; + scopeId = scope_id; + this.proxy = proxy; + this.ctx = ctx; + this.logger = logger; + scopeCache = ctx.GetCacheForScope(scope_id); + } + + // Checks Locals, followed by `this` + public async Task Resolve(string var_name, CancellationToken token) + { + if (scopeCache.Locals.Count == 0 && !locals_fetched) + { + var scope_res = await proxy.GetScopeProperties(messageId, scopeId, token); + if (scope_res.IsErr) + throw new Exception($"BUG: Unable to get properties for scope: {scopeId}. {scope_res}"); + locals_fetched = true; + } + + if (scopeCache.Locals.TryGetValue(var_name, out var obj)) + { + return obj["value"]?.Value(); + } + + if (scopeCache.MemberReferences.TryGetValue(var_name, out var ret)) + return ret; + + if (varIds == null) + { + var scope = ctx.CallStack.FirstOrDefault(s => s.Id == scopeId); + varIds = scope.Method.GetLiveVarsAt(scope.Location.CliLocation.Offset); + } + + var res = await proxy.SendMonoCommand(messageId, MonoCommands.EvaluateMemberAccess(scopeId, var_name, varIds), token); + if (res.IsOk) + { + ret = res.Value?["result"]?["value"]?["value"]?.Value(); + scopeCache.MemberReferences[var_name] = ret; + } + else + { + logger.LogDebug(res.Error.ToString()); + } + + return ret; + } + + } +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs index 4166c7ef236655..afd7d9602e2340 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -426,7 +426,9 @@ protected override async Task AcceptCommand(MessageId id, string method, J async Task RuntimeGetProperties(MessageId id, DotnetObjectId objectId, JToken args, CancellationToken token) { if (objectId.Scheme == "scope") + { return await GetScopeProperties(id, int.Parse(objectId.Value), token); + } var res = await SendMonoCommand(id, MonoCommands.GetDetails(objectId, args), token); if (res.IsErr) @@ -673,64 +675,6 @@ async Task Step(MessageId msg_id, StepKind kind, CancellationToken token) return true; } - internal bool TryFindVariableValueInCache(ExecutionContext ctx, string expression, bool only_search_on_this, out JToken obj) - { - if (ctx.LocalsCache.TryGetValue(expression, out obj)) - { - if (only_search_on_this && obj["fromThis"] == null) - return false; - return true; - } - return false; - } - - internal async Task TryGetVariableValue(MessageId msg_id, int scope_id, string expression, bool only_search_on_this, CancellationToken token) - { - JToken thisValue = null; - var context = GetContext(msg_id); - if (context.CallStack == null) - return null; - - if (TryFindVariableValueInCache(context, expression, only_search_on_this, out JToken obj)) - return obj; - - var scope = context.CallStack.FirstOrDefault(s => s.Id == scope_id); - var live_vars = scope.Method.GetLiveVarsAt(scope.Location.CliLocation.Offset); - //get_this - var res = await SendMonoCommand(msg_id, MonoCommands.GetScopeVariables(scope.Id, live_vars), token); - - var scope_values = res.Value?["result"]?["value"]?.Values()?.ToArray(); - thisValue = scope_values?.FirstOrDefault(v => v["name"]?.Value() == "this"); - - if (!only_search_on_this) - { - if (thisValue != null && expression == "this") - return thisValue; - - var value = scope_values.SingleOrDefault(sv => sv["name"]?.Value() == expression); - if (value != null) - return value; - } - - //search in scope - if (thisValue != null) - { - if (!DotnetObjectId.TryParse(thisValue["value"]["objectId"], out var objectId)) - return null; - - res = await SendMonoCommand(msg_id, MonoCommands.GetDetails(objectId), token); - scope_values = res.Value?["result"]?["value"]?.Values().ToArray(); - var foundValue = scope_values.FirstOrDefault(v => v["name"].Value() == expression); - if (foundValue != null) - { - foundValue["fromThis"] = true; - context.LocalsCache[foundValue["name"].Value()] = foundValue; - return foundValue; - } - } - return null; - } - async Task OnEvaluateOnCallFrame(MessageId msg_id, int scope_id, string expression, CancellationToken token) { try @@ -739,35 +683,40 @@ async Task OnEvaluateOnCallFrame(MessageId msg_id, int scope_id, string ex if (context.CallStack == null) return false; - var varValue = await TryGetVariableValue(msg_id, scope_id, expression, false, token); + var resolver = new MemberReferenceResolver(this, context, msg_id, scope_id, logger); - if (varValue != null) + JObject retValue = await resolver.Resolve(expression, token); + if (retValue == null) + { + retValue = await EvaluateExpression.CompileAndRunTheExpression(expression, resolver, token); + } + + if (retValue != null) { SendResponse(msg_id, Result.OkFromObject(new { - result = varValue["value"] + result = retValue }), token); - return true; } - - string retValue = await EvaluateExpression.CompileAndRunTheExpression(this, msg_id, scope_id, expression, token); - SendResponse(msg_id, Result.OkFromObject(new + else { - result = new - { - value = retValue - } - }), token); - return true; + SendResponse(msg_id, Result.Err($"Unable to evaluate {expression}"), token); + } + } + catch (ReturnAsErrorException ree) + { + SendResponse(msg_id, ree.Error, token); } catch (Exception e) { - logger.LogDebug(e, $"Error in EvaluateOnCallFrame for expression '{expression}."); + logger.LogDebug($"Error in EvaluateOnCallFrame for expression '{expression}' with '{e}."); + SendResponse(msg_id, Result.Exception(e), token); } - return false; + + return true; } - async Task GetScopeProperties(MessageId msg_id, int scope_id, CancellationToken token) + internal async Task GetScopeProperties(MessageId msg_id, int scope_id, CancellationToken token) { try { @@ -788,8 +737,11 @@ async Task GetScopeProperties(MessageId msg_id, int scope_id, Cancellati if (values == null || values.Length == 0) return Result.OkFromObject(new { result = Array.Empty() }); + var frameCache = ctx.GetCacheForScope(scope_id); foreach (var value in values) - ctx.LocalsCache[value["name"]?.Value()] = value; + { + frameCache.Locals[value["name"]?.Value()] = value; + } return Result.OkFromObject(new { result = values }); } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs index dad28dff6f3d15..87218102b1ce29 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using Microsoft.WebAssembly.Diagnostics; @@ -10,205 +12,446 @@ namespace DebuggerTests { - + // TODO: static async, static method args public class EvaluateOnCallFrameTests : DebuggerTestBase { + public static IEnumerable InstanceMethodsTestData(string type_name) + { + yield return new object[] { type_name, "InstanceMethod", "InstanceMethod", false }; + yield return new object[] { type_name, "GenericInstanceMethod", "GenericInstanceMethod", false }; + yield return new object[] { type_name, "InstanceMethodAsync", "MoveNext", true }; + yield return new object[] { type_name, "GenericInstanceMethodAsync", "MoveNext", true }; - [Fact] - public async Task EvaluateThisProperties() => await CheckInspectLocalsAtBreakpointSite( - "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16, - "run", - "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", + // TODO: { "DebuggerTests.EvaluateTestsGeneric`1", "Instance", 9, "EvaluateTestsGenericStructInstanceMethod", prefix } + } + + public static IEnumerable InstanceMethodForTypeMembersTestData(string type_name) + { + foreach (var data in InstanceMethodsTestData(type_name)) + { + yield return new object[] { "", 0 }.Concat(data).ToArray(); + yield return new object[] { "this.", 0 }.Concat(data).ToArray(); + yield return new object[] { "NewInstance.", 3 }.Concat(data).ToArray(); + yield return new object[] { "this.NewInstance.", 3 }.Concat(data).ToArray(); + } + } + + [Theory] + [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsStructWithProperties")] + [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsClassWithProperties")] + public async Task EvaluateTypeInstanceMembers(string prefix, int bias, string type, string method, string bp_function_name, bool is_async) + => await CheckInspectLocalsAtBreakpointSite( + type, method, /*line_offset*/1, bp_function_name, + $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] {type}:run');}}, 1);", wait_for_event_fn: async (pause_location) => { - var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); - var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "a"); - CheckContentValue(evaluate, "1"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "b"); - CheckContentValue(evaluate, "2"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "c"); - CheckContentValue(evaluate, "3"); - - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "dt"); - await CheckDateTimeValue(evaluate, new DateTime(2000, 5, 4, 3, 2, 1)); + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + var dateTime = new DateTime(2010, 9, 8, 7, 6, 5 + bias); + var DTProp = dateTime.AddMinutes(10); + + await EvaluateOnCallFrameAndCheck(id, + (prefix + "a", TNumber(4)), + + // fields + (prefix + "dateTime.TimeOfDay", TValueType("System.TimeSpan", dateTime.TimeOfDay.ToString())), + (prefix + "dateTime", TDateTime(dateTime)), + (prefix + "dateTime.TimeOfDay.Minutes", TNumber(dateTime.TimeOfDay.Minutes)), + + // properties + (prefix + "DTProp.TimeOfDay.Minutes", TNumber(DTProp.TimeOfDay.Minutes)), + (prefix + "DTProp", TDateTime(DTProp)), + (prefix + "DTProp.TimeOfDay", TValueType("System.TimeSpan", DTProp.TimeOfDay.ToString())), + + (prefix + "IntProp", TNumber(9)), + (prefix + "NullIfAIsNotZero", TObject("DebuggerTests.EvaluateTestsClassWithProperties", is_null: true)) + ); }); [Theory] - [InlineData(63, 12, "EvaluateTestsStructInstanceMethod")] - [InlineData(79, 12, "GenericInstanceMethodOnStruct")] - [InlineData(102, 12, "EvaluateTestsGenericStructInstanceMethod")] - public async Task EvaluateThisPropertiesOnStruct(int line, int col, string method_name) => await CheckInspectLocalsAtBreakpointSite( - "dotnet://debugger-test.dll/debugger-evaluate-test.cs", line, col, - method_name, - "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", + [MemberData(nameof(InstanceMethodsTestData), parameters: "DebuggerTests.EvaluateTestsStructWithProperties")] + [MemberData(nameof(InstanceMethodsTestData), parameters: "DebuggerTests.EvaluateTestsClassWithProperties")] + public async Task EvaluateInstanceMethodArguments(string type, string method, string bp_function_name, bool is_async) + => await CheckInspectLocalsAtBreakpointSite( + type, method, /*line_offset*/1, bp_function_name, + $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] {type}:run');}}, 1);", wait_for_event_fn: async (pause_location) => { - var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "a"); - CheckContentValue(evaluate, "1"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "b"); - CheckContentValue(evaluate, "2"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "c"); - CheckContentValue(evaluate, "3"); - - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "dateTime"); - await CheckDateTimeValue(evaluate, new DateTime(2020, 1, 2, 3, 4, 5)); + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + var DTProp = new DateTime(2010, 9, 8, 7, 6, 5).AddMinutes(10); + await EvaluateOnCallFrameAndCheck(id, + ("g", TNumber(400)), + ("h", TNumber(123)), + ("valString", TString("just a test")), + ("me", TObject(type)), + + // property on method arg + ("me.DTProp", TDateTime(DTProp)), + ("me.DTProp.TimeOfDay.Minutes", TNumber(DTProp.TimeOfDay.Minutes)), + ("me.DTProp.Second + (me.IntProp - 5)", TNumber(DTProp.Second + 4))); + }); + + [Theory] + [MemberData(nameof(InstanceMethodsTestData), parameters: "DebuggerTests.EvaluateTestsStructWithProperties")] + [MemberData(nameof(InstanceMethodsTestData), parameters: "DebuggerTests.EvaluateTestsClassWithProperties")] + public async Task EvaluateMethodLocals(string type, string method, string bp_function_name, bool is_async) + => await CheckInspectLocalsAtBreakpointSite( + type, method, /*line_offset*/5, bp_function_name, + $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] {type}:run');}}, 1);", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + var dt = new DateTime(2025, 3, 5, 7, 9, 11); + await EvaluateOnCallFrameAndCheck(id, + ("d", TNumber(401)), + ("e", TNumber(402)), + ("f", TNumber(403)), + + // property on a local + ("local_dt", TDateTime(dt)), + ("local_dt.Date", TDateTime(dt.Date))); }); [Fact] - public async Task EvaluateParameters() => await CheckInspectLocalsAtBreakpointSite( - "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16, - "run", - "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", + public async Task EvaluateStaticLocalsWithDeepMemberAccess() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateTestsClass", "EvaluateLocals", 9, "EvaluateLocals", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", wait_for_event_fn: async (pause_location) => { - var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); - var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "g"); - CheckContentValue(evaluate, "100"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "h"); - CheckContentValue(evaluate, "200"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "valString"); - CheckContentValue(evaluate, "test"); + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + var dt = new DateTime(2020, 1, 2, 3, 4, 5); + await EvaluateOnCallFrameAndCheck(id, + ("f_s.c", TNumber(4)), + ("f_s", TValueType("DebuggerTests.EvaluateTestsStructWithProperties")), + + ("f_s.dateTime", TDateTime(dt)), + ("f_s.dateTime.Date", TDateTime(dt.Date))); }); [Fact] - public async Task EvaluateLocals() => await CheckInspectLocalsAtBreakpointSite( - "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16, - "run", - "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", + public async Task EvaluateLocalsAsync() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.Point", "AsyncInstanceMethod", 1, "MoveNext", + "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.ArrayTestsClass:EntryPointForStructMethod', true); })", wait_for_event_fn: async (pause_location) => { - var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); - var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "d"); - CheckContentValue(evaluate, "101"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "e"); - CheckContentValue(evaluate, "102"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "f"); - CheckContentValue(evaluate, "103"); - - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "local_dt"); - await CheckDateTimeValue(evaluate, new DateTime(2010, 9, 8, 7, 6, 5)); + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + // sc_arg + { + var (sc_arg, _) = await EvaluateOnCallFrame(id, "sc_arg"); + await CheckValue(sc_arg, TObject("DebuggerTests.SimpleClass"), nameof(sc_arg)); + + // Check that we did get the correct object + var sc_arg_props = await GetProperties(sc_arg["objectId"]?.Value()); + await CheckProps(sc_arg_props, new + { + X = TNumber(10), + Y = TNumber(45), + Id = TString("sc#Id"), + Color = TEnum("DebuggerTests.RGB", "Blue"), + PointWithCustomGetter = TGetter("PointWithCustomGetter") + }, "sc_arg_props#1"); + + await EvaluateOnCallFrameAndCheck(id, + ("(sc_arg.PointWithCustomGetter.X)", TNumber(100)), + ("sc_arg.Id + \"_foo\"", TString($"sc#Id_foo")), + ("sc_arg.Id + (sc_arg.X==10 ? \"_is_ten\" : \"_not_ten\")", TString($"sc#Id_is_ten"))); + } + + // local_gs + { + var (local_gs, _) = await EvaluateOnCallFrame(id, "local_gs"); + await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct"), nameof(local_gs)); + + var local_gs_props = await GetProperties(local_gs["objectId"]?.Value()); + await CheckProps(local_gs_props, new + { + Id = TObject("string", is_null: true), + Color = TEnum("DebuggerTests.RGB", "Red"), + Value = TNumber(0) + }, "local_gs_props#1"); + await EvaluateOnCallFrameAndCheck(id, ("(local_gs.Id)", TString(null))); + } + }); + + [Theory] + [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsStructWithProperties")] + [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsClassWithProperties")] + public async Task EvaluateExpressionsWithDeepMemberAccesses(string prefix, int bias, string type, string method, string bp_function_name, bool _) + => await CheckInspectLocalsAtBreakpointSite( + type, method, /*line_offset*/4, bp_function_name, + $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] {type}:run');}}, 1);", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + var dateTime = new DateTime(2010, 9, 8, 7, 6, 5 + bias); + var DTProp = dateTime.AddMinutes(10); + + await EvaluateOnCallFrameAndCheck(id, + ($"{prefix}a + 5", TNumber(9)), + ($"10 + {prefix}IntProp", TNumber(19)), + ($" {prefix}IntProp + {prefix}DTProp.Second", TNumber(9 + DTProp.Second)), + ($" {prefix}IntProp + ({prefix}DTProp.Second+{prefix}dateTime.Year)", TNumber(9 + DTProp.Second + dateTime.Year)), + ($" {prefix}DTProp.Second > 0 ? \"_foo_\": \"_zero_\"", TString("_foo_")), + + // local_dt is not live yet + ($"local_dt.Date.Year * 10", TNumber(10))); }); [Fact] - public async Task EvaluateLocalsAsync() + public async Task EvaluateSimpleExpressions() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateTestsClass/TestEvaluate", "run", 9, "run", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + await EvaluateOnCallFrameAndCheck(id, + // "((this))", TObject("foo")); //FIXME: + // "((dt))", TObject("foo")); //FIXME: + + ("this", TObject("DebuggerTests.EvaluateTestsClass.TestEvaluate")), + + ("5", TNumber(5)), + ("d + e", TNumber(203)), + ("e + 10", TNumber(112)), + + // repeated expressions + ("this.a + this.a", TNumber(2)), + ("a + \"_\" + a", TString("9000_9000")), + ("a+(a )", TString("90009000")), + + // possible duplicate arg name + ("this.a + this_a", TNumber(46)), + + ("this.a + this.b", TNumber(3)), + ("\"test\" + \"test\"", TString("testtest")), + ("5 + 5", TNumber(10))); + }); + + public static TheoryData ShadowMethodArgsTestData => new TheoryData { - var bp_loc = "dotnet://debugger-test.dll/debugger-array-test.cs"; - int line = 249; - int col = 12; - var function_name = "MoveNext"; - await CheckInspectLocalsAtBreakpointSite( - bp_loc, line, col, - function_name, - "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.ArrayTestsClass:EntryPointForStructMethod', true); })", - wait_for_event_fn: async (pause_location) => + { "DebuggerTests.EvaluateTestsClassWithProperties", "EvaluateShadow", "EvaluateShadow" }, + { "DebuggerTests.EvaluateTestsClassWithProperties", "EvaluateShadowAsync", "MoveNext" }, + { "DebuggerTests.EvaluateTestsStructWithProperties", "EvaluateShadow", "EvaluateShadow" }, + { "DebuggerTests.EvaluateTestsStructWithProperties", "EvaluateShadowAsync", "MoveNext" }, + }; + + [Theory] + [MemberData(nameof(ShadowMethodArgsTestData))] + public async Task LocalsAndArgsShadowingThisMembers(string type_name, string method, string bp_function_name) => await CheckInspectLocalsAtBreakpointSite( + type_name, method, 2, bp_function_name, + "window.setTimeout(function() { invoke_static_method ('[debugger-test] " + type_name + ":run'); })", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + await EvaluateOnCallFrameAndCheck(id, + ("a", TString("hello")), + ("this.a", TNumber(4))); + + await CheckExpressions("this.", new DateTime(2010, 9, 8, 7, 6, 5 + 0)); + await CheckExpressions(String.Empty, new DateTime(2020, 3, 4, 5, 6, 7)); + + async Task CheckExpressions(string prefix, DateTime dateTime) { - var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + await EvaluateOnCallFrameAndCheck(id, + (prefix + "dateTime", TDateTime(dateTime)), + (prefix + "dateTime.TimeOfDay.Minutes", TNumber(dateTime.TimeOfDay.Minutes)), + (prefix + "dateTime.TimeOfDay", TValueType("System.TimeSpan", dateTime.TimeOfDay.ToString()))); + } + }); - // sc_arg - { - var sc_arg = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "sc_arg"); - await CheckValue(sc_arg, TObject("DebuggerTests.SimpleClass"), "sc_arg#1"); - - var sc_arg_props = await GetProperties(sc_arg["objectId"]?.Value()); - await CheckProps(sc_arg_props, new - { - X = TNumber(10), - Y = TNumber(45), - Id = TString("sc#Id"), - Color = TEnum("DebuggerTests.RGB", "Blue"), - PointWithCustomGetter = TGetter("PointWithCustomGetter") - }, "sc_arg_props#1"); - } - - // local_gs - { - var local_gs = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "local_gs"); - await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct"), "local_gs#1"); - - var local_gs_props = await GetProperties(local_gs["objectId"]?.Value()); - await CheckProps(local_gs_props, new - { - Id = TObject("string", is_null: true), - Color = TEnum("DebuggerTests.RGB", "Red"), - Value = TNumber(0) - }, "local_gs_props#1"); - } - - // step, check local_gs - pause_location = await StepAndCheck(StepKind.Over, bp_loc, line + 1, col, function_name); - { - var local_gs = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "local_gs"); - await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct"), "local_gs#2"); - - var local_gs_props = await GetProperties(local_gs["objectId"]?.Value()); - await CheckProps(local_gs_props, new - { - Id = TString("local_gs#Id"), - Color = TEnum("DebuggerTests.RGB", "Green"), - Value = TNumber(4) - }, "local_gs_props#2"); - } - - // step check sc_arg.Id - pause_location = await StepAndCheck(StepKind.Over, bp_loc, line + 2, col, function_name); - { - var sc_arg = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "sc_arg"); - await CheckValue(sc_arg, TObject("DebuggerTests.SimpleClass"), "sc_arg#2"); - - var sc_arg_props = await GetProperties(sc_arg["objectId"]?.Value()); - await CheckProps(sc_arg_props, new - { - X = TNumber(10), - Y = TNumber(45), - Id = TString("sc_arg#Id"), // <------- This changed - Color = TEnum("DebuggerTests.RGB", "Blue"), - PointWithCustomGetter = TGetter("PointWithCustomGetter") - }, "sc_arg_props#2"); - } - }); + [Theory] + [InlineData("DebuggerTests.EvaluateTestsStructWithProperties", true)] + [InlineData("DebuggerTests.EvaluateTestsClassWithProperties", false)] + public async Task EvaluateOnPreviousFrames(string type_name, bool is_valuetype) => await CheckInspectLocalsAtBreakpointSite( + type_name, "EvaluateShadow", 1, "EvaluateShadow", + $"window.setTimeout(function() {{ invoke_static_method ('[debugger-test] {type_name}:run'); }})", + wait_for_event_fn: async (pause_location) => + { + var dt_local = new DateTime(2020, 3, 4, 5, 6, 7); + var dt_this = new DateTime(2010, 9, 8, 7, 6, 5); + + // At EvaluateShadow + { + var id0 = pause_location["callFrames"][0]["callFrameId"].Value(); + await EvaluateOnCallFrameAndCheck(id0, + ("dateTime", TDateTime(dt_local)), + ("this.dateTime", TDateTime(dt_this)) + ); + + await EvaluateOnCallFrameFail(id0, ("obj.IntProp", "ReferenceError")); + } + + { + var id1 = pause_location["callFrames"][1]["callFrameId"].Value(); + await EvaluateOnCallFrameFail(id1, + ("dateTime", "ReferenceError"), + ("this.dateTime", "ReferenceError")); + + // obj available only on the -1 frame + await EvaluateOnCallFrameAndCheck(id1, ("obj.IntProp", TNumber(7))); + } + + await SetBreakpointInMethod("debugger-test.dll", type_name, "SomeMethod", 1); + pause_location = await SendCommandAndCheck(null, "Debugger.resume", null, 0, 0, "SomeMethod"); + + // At SomeMethod + + // TODO: change types also.. so, that `this` is different! + + // Check frame0 + { + var id0 = pause_location["callFrames"][0]["callFrameId"].Value(); + + // 'me' and 'dateTime' are reversed in this method + await EvaluateOnCallFrameAndCheck(id0, + ("dateTime", is_valuetype ? TValueType(type_name) : TObject(type_name)), + ("this.dateTime", TDateTime(dt_this)), + ("me", TDateTime(dt_local)), + + // local variable shadows field, but isn't "live" yet + ("DTProp", TString(null)), + + // access field via `this.` + ("this.DTProp", TDateTime(dt_this.AddMinutes(10)))); + + await EvaluateOnCallFrameFail(id0, ("obj", "ReferenceError")); + } + + // check frame1 + { + var id1 = pause_location["callFrames"][1]["callFrameId"].Value(); + + await EvaluateOnCallFrameAndCheck(id1, + // 'me' and 'dateTime' are reversed in this method + ("dateTime", TDateTime(dt_local)), + ("this.dateTime", TDateTime(dt_this)), + ("me", is_valuetype ? TValueType(type_name) : TObject(type_name)), + + // not shadowed here + ("DTProp", TDateTime(dt_this.AddMinutes(10))), + + // access field via `this.` + ("this.DTProp", TDateTime(dt_this.AddMinutes(10)))); + + await EvaluateOnCallFrameFail(id1, ("obj", "ReferenceError")); + } + + // check frame2 + { + var id2 = pause_location["callFrames"][2]["callFrameId"].Value(); + + // Only obj should be available + await EvaluateOnCallFrameFail(id2, + ("dateTime", "ReferenceError"), + ("this.dateTime", "ReferenceError"), + ("me", "ReferenceError")); + + await EvaluateOnCallFrameAndCheck(id2, ("obj", is_valuetype ? TValueType(type_name) : TObject(type_name))); + } + }); + + [Fact] + public async Task JSEvaluate() + { + var insp = new Inspector(); + //Collect events + var scripts = SubscribeToScripts(insp); + + var bp_loc = "/other.js"; + var line = 76; + var col = 1; + + await Ready(); + await insp.Ready(async (cli, token) => + { + ctx = new DebugTestContext(cli, insp, token, scripts); + await SetBreakpoint(bp_loc, line, col); + + var eval_expr = "window.setTimeout(function() { eval_call_on_frame_test (); }, 1)"; + var result = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = eval_expr }), ctx.token); + var pause_location = await ctx.insp.WaitFor(Inspector.PAUSE); + + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + await EvaluateOnCallFrameFail(id, + ("me.foo", null), + ("obj.foo.bar", null)); + + await EvaluateOnCallFrame(id, "obj.foo", expect_ok: true); + }); } [Fact] - public async Task EvaluateExpressions() => await CheckInspectLocalsAtBreakpointSite( - "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16, - "run", - "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", + public async Task NegativeTestsInInstanceMethod() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateTestsClass/TestEvaluate", "run", 9, "run", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", wait_for_event_fn: async (pause_location) => { - var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); - var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "d + e"); - CheckContentValue(evaluate, "203"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "e + 10"); - CheckContentValue(evaluate, "112"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "a + a"); - CheckContentValue(evaluate, "2"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "this.a + this.b"); - CheckContentValue(evaluate, "3"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "\"test\" + \"test\""); - CheckContentValue(evaluate, "testtest"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "5 + 5"); - CheckContentValue(evaluate, "10"); + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + // Use '.' on a primitive member + await EvaluateOnCallFrameFail(id, + //BUG: TODO: + //("a)", "CompilationError"), + + ("this.a.", "ReferenceError"), + ("a.", "ReferenceError"), + + ("this..a", "CompilationError"), + (".a.", "ReferenceError"), + + ("me.foo", "ReferenceError"), + + ("this.a + non_existant", "ReferenceError"), + + ("this.NullIfAIsNotZero.foo", "ReferenceError"), + ("NullIfAIsNotZero.foo", "ReferenceError")); }); [Fact] - public async Task EvaluateThisExpressions() => await CheckInspectLocalsAtBreakpointSite( - "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16, - "run", - "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", + public async Task NegativeTestsInStaticMethod() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateTestsClass", "EvaluateLocals", 9, "EvaluateLocals", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", wait_for_event_fn: async (pause_location) => { - var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); - var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "this.a"); - CheckContentValue(evaluate, "1"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "this.b"); - CheckContentValue(evaluate, "2"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "this.c"); - CheckContentValue(evaluate, "3"); - - // FIXME: not supported yet - // evaluate = await EvaluateOnCallFrame (pause_location ["callFrames"][0] ["callFrameId"].Value (), "this.dt"); - // await CheckDateTimeValue (evaluate, new DateTime (2000, 5, 4, 3, 2, 1)); + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + await EvaluateOnCallFrameFail(id, + ("me.foo", "ReferenceError"), + ("this", "ReferenceError"), + ("this.NullIfAIsNotZero.foo", "ReferenceError")); }); + + async Task EvaluateOnCallFrameAndCheck(string call_frame_id, params (string expression, JObject expected)[] args) + { + foreach (var arg in args) + { + var (eval_val, _) = await EvaluateOnCallFrame(call_frame_id, arg.expression); + try + { + await CheckValue(eval_val, arg.expected, arg.expression); + } + catch + { + Console.WriteLine($"CheckValue failed for {arg.expression}. Expected: {arg.expected}, vs {eval_val}"); + throw; + } + } + } + + async Task EvaluateOnCallFrameFail(string call_frame_id, params (string expression, string class_name)[] args) + { + foreach (var arg in args) + { + var (_, res) = await EvaluateOnCallFrame(call_frame_id, arg.expression, expect_ok: false); + if (arg.class_name != null) + AssertEqual(arg.class_name, res.Error["result"]?["className"]?.Value(), $"Error className did not match for expression '{arg.expression}'"); + } + } } } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/Support.cs b/src/mono/wasm/debugger/DebuggerTestSuite/Support.cs index d7df33589913d7..81501e443d13e0 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/Support.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/Support.cs @@ -14,6 +14,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; +using Xunit.Sdk; namespace DebuggerTests { @@ -338,6 +339,12 @@ internal async Task CheckPointerValue(JToken locals, string name, JToken return l; } + internal async Task CheckDateTime(JToken value, DateTime expected, string label = "") + { + await CheckValue(value, TValueType("System.DateTime", expected.ToString()), label); + await CheckDateTimeValue(value, expected); + } + internal async Task CheckDateTime(JToken locals, string name, DateTime expected) { var obj = GetAndAssertObjectWithName(locals, name); @@ -616,6 +623,13 @@ internal async Task CheckCustomType(JToken actual_val, JToken exp_val, string la break; } + case "datetime": + { + var dateTime = DateTime.FromBinary(exp_val["binary"].Value()); + await CheckDateTime(actual_val, dateTime, label); + break; + } + case "ignore_me": // nothing to check ;) break; @@ -822,7 +836,7 @@ internal async Task GetProperties(string id, JToken fn_args = null, bool return locals; } - internal async Task EvaluateOnCallFrame(string id, string expression) + internal async Task<(JToken, Result)> EvaluateOnCallFrame(string id, string expression, bool expect_ok = true) { var evaluate_req = JObject.FromObject(new { @@ -830,12 +844,12 @@ internal async Task EvaluateOnCallFrame(string id, string expression) expression = expression }); - var frame_evaluate = await ctx.cli.SendCommand("Debugger.evaluateOnCallFrame", evaluate_req, ctx.token); - if (!frame_evaluate.IsOk) - Assert.True(false, $"Debugger.evaluateOnCallFrame failed for {evaluate_req.ToString()}, with Result: {frame_evaluate}"); + var res = await ctx.cli.SendCommand("Debugger.evaluateOnCallFrame", evaluate_req, ctx.token); + AssertEqual(expect_ok, res.IsOk, $"Debugger.evaluateOnCallFrame ('{expression}', scope: {id}) returned {res.IsOk} instead of {expect_ok}, with Result: {res}"); + if (res.IsOk) + return (res.Value["result"], res); - var evaluate_result = frame_evaluate.Value["result"]; - return evaluate_result; + return (null, res); } internal async Task SetBreakpoint(string url_key, int line, int column, bool expect_ok = true, bool use_regex = false) @@ -880,10 +894,15 @@ internal async Task SetBreakpointInMethod(string assembly, string type, return res; } - internal void AssertEqual(object expected, object actual, string label) => Assert.True(expected?.Equals(actual), - $"[{label}]\n" + - $"Expected: {expected?.ToString()}\n" + - $"Actual: {actual?.ToString()}\n"); + internal void AssertEqual(object expected, object actual, string label) + { + if (expected?.Equals(actual) == true) + return; + + throw new AssertActualExpectedException( + expected, actual, + $"[{label}]\n"); + } internal void AssertStartsWith(string expected, string actual, string label) => Assert.True(actual?.StartsWith(expected), $"[{label}] Does not start with the expected string\nExpected: {expected}\nActual: {actual}"); @@ -907,7 +926,7 @@ internal void AssertEqual(object expected, object actual, string label) => Asser //FIXME: um maybe we don't need to convert jobject right here! internal static JObject TString(string value) => value == null ? - TObject("string", is_null: true) : + JObject.FromObject(new { type = "object", className = "string", subtype = "null" }) : JObject.FromObject(new { type = "string", value = @value }); internal static JObject TNumber(int value) => @@ -951,6 +970,12 @@ internal static JObject TDelegate(string className, string target) => JObject.Fr internal static JObject TIgnore() => JObject.FromObject(new { __custom_type = "ignore_me" }); internal static JObject TGetter(string type) => JObject.FromObject(new { __custom_type = "getter", type_name = type }); + + internal static JObject TDateTime(DateTime dt) => JObject.FromObject(new + { + __custom_type = "datetime", + binary = dt.ToBinary() + }); } class DebugTestContext diff --git a/src/mono/wasm/debugger/tests/debugger-evaluate-test.cs b/src/mono/wasm/debugger/tests/debugger-evaluate-test.cs index ffb6fadafb18ef..498c02832e335f 100644 --- a/src/mono/wasm/debugger/tests/debugger-evaluate-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-evaluate-test.cs @@ -13,17 +13,18 @@ public class TestEvaluate public int b; public int c; public DateTime dt = new DateTime(2000, 5, 4, 3, 2, 1); - public void run(int g, int h, string valString) + public TestEvaluate NullIfAIsNotZero => a != 0 ? null : this; + public void run(int g, int h, string a, string valString, int this_a) { int d = g + 1; int e = g + 2; int f = g + 3; int i = d + e + f; var local_dt = new DateTime(2010, 9, 8, 7, 6, 5); - a = 1; + this.a = 1; b = 2; c = 3; - a = a + 1; + this.a = this.a + 1; b = b + 1; c = c + 1; } @@ -32,31 +33,103 @@ public void run(int g, int h, string valString) public static void EvaluateLocals() { TestEvaluate f = new TestEvaluate(); - f.run(100, 200, "test"); + f.run(100, 200, "9000", "test", 45); - var f_s = new EvaluateTestsStruct(); - f_s.EvaluateTestsStructInstanceMethod(100, 200, "test"); - f_s.GenericInstanceMethodOnStruct(100, 200, "test"); + var f_s = new EvaluateTestsStructWithProperties(); + f_s.InstanceMethod(100, 200, "test", f_s); + f_s.GenericInstanceMethod(100, 200, "test", f_s); var f_g_s = new EvaluateTestsGenericStruct(); f_g_s.EvaluateTestsGenericStructInstanceMethod(100, 200, "test"); - Console.WriteLine($"a: {f.a}, b: {f.b}, c: {f.c}"); } } - public struct EvaluateTestsStruct + public struct EvaluateTestsGenericStruct { public int a; public int b; public int c; DateTime dateTime; - public void EvaluateTestsStructInstanceMethod(int g, int h, string valString) + public void EvaluateTestsGenericStructInstanceMethod(int g, int h, string valString) { int d = g + 1; int e = g + 2; int f = g + 3; - int i = d + e + f; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); + a = 1; + b = 2; + c = 3; + dateTime = new DateTime(2020, 1, 2, 3, 4, 5); + T t = default(T); + a = a + 1; + b = b + 2; + c = c + 3; + } + } + + public class EvaluateTestsClassWithProperties + { + public int a; + public int b; + public int c { get; set; } + + public DateTime dateTime; + public DateTime DTProp => dateTime.AddMinutes(10); + public int IntProp => a + 5; + public string SetOnlyProp { set { a = value.Length; } } + public EvaluateTestsClassWithProperties NullIfAIsNotZero => a != 1908712 ? null : new EvaluateTestsClassWithProperties(0); + public EvaluateTestsClassWithProperties NewInstance => new EvaluateTestsClassWithProperties(3); + + public EvaluateTestsClassWithProperties(int bias) + { + a = 4; + b = 0; + c = 0; + dateTime = new DateTime(2010, 9, 8, 7, 6, 5 + bias); + } + + public static async Task run() + { + var obj = new EvaluateTestsClassWithProperties(0); + var obj2 = new EvaluateTestsClassWithProperties(0); + obj.InstanceMethod(400, 123, "just a test", obj2); + new EvaluateTestsClassWithProperties(0).GenericInstanceMethod(400, 123, "just a test", obj2); + new EvaluateTestsClassWithProperties(0).EvaluateShadow(new DateTime(2020, 3, 4, 5, 6, 7), obj.NewInstance); + + await new EvaluateTestsClassWithProperties(0).InstanceMethodAsync(400, 123, "just a test", obj2); + await new EvaluateTestsClassWithProperties(0).GenericInstanceMethodAsync(400, 123, "just a test", obj2); + await new EvaluateTestsClassWithProperties(0).EvaluateShadowAsync(new DateTime(2020, 3, 4, 5, 6, 7), obj.NewInstance); + } + + public void EvaluateShadow(DateTime dateTime, EvaluateTestsClassWithProperties me) + { + string a = "hello"; + Console.WriteLine($"Evaluate - break here"); + SomeMethod(dateTime, me); + } + + public async Task EvaluateShadowAsync(DateTime dateTime, EvaluateTestsClassWithProperties me) + { + string a = "hello"; + Console.WriteLine($"EvaluateShadowAsync - break here"); + await Task.CompletedTask; + } + + public void SomeMethod(DateTime me, EvaluateTestsClassWithProperties dateTime) + { + Console.WriteLine($"break here"); + + var DTProp = "hello"; + Console.WriteLine($"dtProp: {DTProp}"); + } + + public async Task InstanceMethodAsync(int g, int h, string valString, EvaluateTestsClassWithProperties me) + { + int d = g + 1; + int e = g + 2; + int f = g + 3; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); a = 1; b = 2; c = 3; @@ -64,45 +137,176 @@ public void EvaluateTestsStructInstanceMethod(int g, int h, string valString) a = a + 1; b = b + 1; c = c + 1; + await Task.CompletedTask; } - public void GenericInstanceMethodOnStruct(int g, int h, string valString) + public void InstanceMethod(int g, int h, string valString, EvaluateTestsClassWithProperties me) { int d = g + 1; int e = g + 2; int f = g + 3; - int i = d + e + f; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); a = 1; b = 2; c = 3; dateTime = new DateTime(2020, 1, 2, 3, 4, 5); + a = a + 1; + b = b + 1; + c = c + 1; + } + + public void GenericInstanceMethod(int g, int h, string valString, EvaluateTestsClassWithProperties me) + { + int d = g + 1; + int e = g + 2; + int f = g + 3; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); + a = 1; + b = 2; + c = 3; + dateTime = new DateTime(2020, 1, 2, 3, 4, 5); + a = a + 1; + b = b + 1; + c = c + 1; T t = default(T); + } + + public async Task GenericInstanceMethodAsync(int g, int h, string valString, EvaluateTestsClassWithProperties me) + { + int d = g + 1; + int e = g + 2; + int f = g + 3; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); + a = 1; + b = 2; + c = 3; + dateTime = new DateTime(2020, 1, 2, 3, 4, 5); a = a + 1; b = b + 1; c = c + 1; + T t = default(T); + return await Task.FromResult(default(T)); } } - public struct EvaluateTestsGenericStruct + public struct EvaluateTestsStructWithProperties { public int a; public int b; - public int c; - DateTime dateTime; - public void EvaluateTestsGenericStructInstanceMethod(int g, int h, string valString) + public int c { get; set; } + + public DateTime dateTime; + public DateTime DTProp => dateTime.AddMinutes(10); + public int IntProp => a + 5; + public string SetOnlyProp { set { a = value.Length; } } + public EvaluateTestsClassWithProperties NullIfAIsNotZero => a != 1908712 ? null : new EvaluateTestsClassWithProperties(0); + public EvaluateTestsStructWithProperties NewInstance => new EvaluateTestsStructWithProperties(3); + + public EvaluateTestsStructWithProperties(int bias) + { + a = 4; + b = 0; + c = 0; + dateTime = new DateTime(2010, 9, 8, 7, 6, 5 + bias); + } + + public static async Task run() + { + var obj = new EvaluateTestsStructWithProperties(0); + var obj2 = new EvaluateTestsStructWithProperties(0); + obj.InstanceMethod(400, 123, "just a test", obj2); + new EvaluateTestsStructWithProperties(0).GenericInstanceMethod(400, 123, "just a test", obj2); + new EvaluateTestsStructWithProperties(0).EvaluateShadow(new DateTime(2020, 3, 4, 5, 6, 7), obj.NewInstance); + + await new EvaluateTestsStructWithProperties(0).InstanceMethodAsync(400, 123, "just a test", obj2); + await new EvaluateTestsStructWithProperties(0).GenericInstanceMethodAsync(400, 123, "just a test", obj2); + await new EvaluateTestsStructWithProperties(0).EvaluateShadowAsync(new DateTime(2020, 3, 4, 5, 6, 7), obj.NewInstance); + } + + public void EvaluateShadow(DateTime dateTime, EvaluateTestsStructWithProperties me) + { + string a = "hello"; + Console.WriteLine($"Evaluate - break here"); + SomeMethod(dateTime, me); + } + + public async Task EvaluateShadowAsync(DateTime dateTime, EvaluateTestsStructWithProperties me) + { + string a = "hello"; + Console.WriteLine($"EvaluateShadowAsync - break here"); + await Task.CompletedTask; + } + + public void SomeMethod(DateTime me, EvaluateTestsStructWithProperties dateTime) + { + Console.WriteLine($"break here"); + + var DTProp = "hello"; + Console.WriteLine($"dtProp: {DTProp}"); + } + + public async Task InstanceMethodAsync(int g, int h, string valString, EvaluateTestsStructWithProperties me) + { + int d = g + 1; + int e = g + 2; + int f = g + 3; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); + a = 1; + b = 2; + c = 3; + dateTime = new DateTime(2020, 1, 2, 3, 4, 5); + a = a + 1; + b = b + 1; + c = c + 1; + await Task.CompletedTask; + } + + public void InstanceMethod(int g, int h, string valString, EvaluateTestsStructWithProperties me) { int d = g + 1; int e = g + 2; int f = g + 3; - int i = d + e + f; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); a = 1; b = 2; c = 3; dateTime = new DateTime(2020, 1, 2, 3, 4, 5); + a = a + 1; + b = b + 1; + c = c + 1; + } + + public void GenericInstanceMethod(int g, int h, string valString, EvaluateTestsStructWithProperties me) + { + int d = g + 1; + int e = g + 2; + int f = g + 3; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); + a = 1; + b = 2; + c = 3; + dateTime = new DateTime(2020, 1, 2, 3, 4, 5); + a = a + 1; + b = b + 1; + c = c + 1; T t = default(T); + } + + public async Task GenericInstanceMethodAsync(int g, int h, string valString, EvaluateTestsStructWithProperties me) + { + int d = g + 1; + int e = g + 2; + int f = g + 3; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); + a = 1; + b = 2; + c = 3; + dateTime = new DateTime(2020, 1, 2, 3, 4, 5); a = a + 1; - b = b + 2; - c = c + 3; + b = b + 1; + c = c + 1; + T t = default(T); + return await Task.FromResult(default(T)); } } } diff --git a/src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs b/src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs index 0c5cc9019fb52b..f557f4e9c3ee63 100644 --- a/src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs @@ -11,7 +11,7 @@ public class ValueTypesTest public static void MethodWithLocalStructs() { var ss_local = new SimpleStruct("set in MethodWithLocalStructs", 1, DateTimeKind.Utc); - var gs_local = new GenericStruct { StringField = "gs_local#GenericStruct#StringField" }; + var gs_local = new GenericStruct { StringField = $"gs_local#GenericStruct#StringField" }; ValueTypesTest vt_local = new ValueTypesTest { diff --git a/src/mono/wasm/debugger/tests/other.js b/src/mono/wasm/debugger/tests/other.js index 93532686914e8e..d3a6d398972697 100644 --- a/src/mono/wasm/debugger/tests/other.js +++ b/src/mono/wasm/debugger/tests/other.js @@ -63,3 +63,16 @@ function negative_cfo_test (str_value = null) { console.log (`break here`); return ptd; } + +function eval_call_on_frame_test () { + let obj = { + a: 5, + b: "hello", + c: { + c_x: 1 + }, + }; + + let obj_undefined = undefined; + console.log(`break here`); +} diff --git a/src/mono/wasm/runtime/library_mono.js b/src/mono/wasm/runtime/library_mono.js index d046a77c92dd9c..f5b506dd5abf18 100644 --- a/src/mono/wasm/runtime/library_mono.js +++ b/src/mono/wasm/runtime/library_mono.js @@ -361,6 +361,73 @@ var MonoSupportLib = { return res; }, + _resolve_member_by_name: function (base_object, base_name, expr_parts) { + if (base_object === undefined || base_object.value === undefined) + throw new Error(`Bug: base_object is undefined`); + + if (base_object.value.type === 'object' && base_object.value.subtype === 'null') + throw new ReferenceError(`Null reference: ${base_name} is null`); + + if (base_object.value.type !== 'object') + throw new ReferenceError(`'.' is only supported on non-primitive types. Failed on '${base_name}'`); + + if (expr_parts.length == 0) + throw new Error(`Invalid member access expression`);//FIXME: need the full expression here + + const root = expr_parts[0]; + const props = this.mono_wasm_get_details(base_object.value.objectId, {}); + let resObject = props.find(l => l.name == root); + if (resObject !== undefined) { + if (resObject.value === undefined && resObject.get !== undefined) + resObject = this._invoke_getter(base_object.value.objectId, root); + } + + if (resObject === undefined || expr_parts.length == 1) + return resObject; + else { + expr_parts.shift(); + return this._resolve_member_by_name(resObject, root, expr_parts); + } + }, + + mono_wasm_eval_member_access: function (scope, var_list, rootObjectId, expr) { + if (expr === undefined || expr.length == 0) + throw new Error(`expression argument required`); + + let parts = expr.split('.'); + if (parts.length == 0) + throw new Error(`Invalid member access expression: ${expr}`); + + const root = parts[0]; + + const locals = this.mono_wasm_get_variables(scope, var_list); + let rootObject = locals.find(l => l.name === root); + if (rootObject === undefined) { + // check `this` + const thisObject = locals.find(l => l.name == "this"); + if (thisObject === undefined) + throw new ReferenceError(`Could not find ${root} in locals, and no 'this' found.`); + + const thisProps = this.mono_wasm_get_details(thisObject.value.objectId, {}); + rootObject = thisProps.find(tp => tp.name == root); + if (rootObject === undefined) + throw new ReferenceError(`Could not find ${root} in locals, or in 'this'`); + + if (rootObject.value === undefined && rootObject.get !== undefined) + rootObject = this._invoke_getter(thisObject.value.objectId, root); + } + + parts.shift(); + + if (parts.length == 0) + return rootObject; + + if (rootObject === undefined || rootObject.value === undefined) + throw new Error(`Could not get a value for ${root}`); + + return this._resolve_member_by_name(rootObject, root, parts); + }, + /** * @param {WasmId} id * @returns {object[]}