diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs deleted file mode 100644 index 54b175b7796fa5..00000000000000 --- a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs +++ /dev/null @@ -1,483 +0,0 @@ -// 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.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -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.Scripting; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Emit; -using Microsoft.CodeAnalysis.Scripting; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.Text.RegularExpressions; -using System.Globalization; - -namespace Microsoft.WebAssembly.Diagnostics -{ - internal static class EvaluateExpression - { - internal static Script script = CSharpScript.Create( - "", - ScriptOptions.Default.WithReferences( - typeof(object).Assembly, - typeof(Enumerable).Assembly, - typeof(JObject).Assembly - )); - private class FindVariableNMethodCall : CSharpSyntaxWalker - { - private static Regex regexForReplaceVarName = new Regex(@"[^A-Za-z0-9_]", RegexOptions.Singleline); - public List identifiers = new List(); - public List methodCall = new List(); - public List memberAccesses = new List(); - public List elementAccess = new List(); - public List argValues = new List(); - public Dictionary memberAccessValues = new Dictionary(); - private int visitCount; - public bool hasMethodCalls; - public bool hasElementAccesses; - internal List variableDefinitions = new List(); - - public void VisitInternal(SyntaxNode node) - { - Visit(node); - visitCount++; - } - public override void Visit(SyntaxNode node) - { - // TODO: PointerMemberAccessExpression - if (visitCount == 0) - { - if (node is MemberAccessExpressionSyntax maes - && node.Kind() == SyntaxKind.SimpleMemberAccessExpression - && !(node.Parent is MemberAccessExpressionSyntax) - && !(node.Parent is InvocationExpressionSyntax) - && !(node.Parent is ElementAccessExpressionSyntax)) - { - memberAccesses.Add(maes); - } - - if (node is IdentifierNameSyntax identifier - && !(identifier.Parent is MemberAccessExpressionSyntax) - && !(identifier.Parent is InvocationExpressionSyntax) - && !(node.Parent is ElementAccessExpressionSyntax) - && !identifiers.Any(x => x.Identifier.Text == identifier.Identifier.Text)) - { - identifiers.Add(identifier); - } - } - - if (node is InvocationExpressionSyntax) - { - if (visitCount == 1) - methodCall.Add(node as InvocationExpressionSyntax); - hasMethodCalls = true; - } - - if (node is ElementAccessExpressionSyntax) - { - if (visitCount == 1) - elementAccess.Add(node as ElementAccessExpressionSyntax); - hasElementAccesses = true; - } - - if (node is AssignmentExpressionSyntax) - throw new Exception("Assignment is not implemented yet"); - base.Visit(node); - } - - public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable ma_values, IEnumerable id_values, IEnumerable method_values, IEnumerable ea_values) - { - var memberAccessToParamName = new Dictionary(); - var methodCallToParamName = new Dictionary(); - var elementAccessToParamName = new Dictionary(); - - CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot(); - - // 1. Replace all this.a occurrences with this_a_ABDE - root = root.ReplaceNodes(memberAccesses, (maes, _) => - { - string ma_str = maes.ToString(); - if (!memberAccessToParamName.TryGetValue(ma_str, out string id_name)) - { - // Generate a random suffix - string suffix = Guid.NewGuid().ToString().Substring(0, 5); - string prefix = regexForReplaceVarName.Replace(ma_str, "_"); - id_name = $"{prefix}_{suffix}"; - - memberAccessToParamName[ma_str] = id_name; - } - - return SyntaxFactory.IdentifierName(id_name); - }); - - // 1.1 Replace all this.a() occurrences with this_a_ABDE - root = root.ReplaceNodes(methodCall, (m, _) => - { - string iesStr = m.ToString(); - if (!methodCallToParamName.TryGetValue(iesStr, out string id_name)) - { - // Generate a random suffix - string suffix = Guid.NewGuid().ToString().Substring(0, 5); - string prefix = regexForReplaceVarName.Replace(iesStr, "_"); - id_name = $"{prefix}_{suffix}"; - methodCallToParamName[iesStr] = id_name; - } - - return SyntaxFactory.IdentifierName(id_name); - }); - - // 1.2 Replace all this.a[x] occurrences with this_a_ABDE - root = root.ReplaceNodes(elementAccess, (ea, _) => - { - string eaStr = ea.ToString(); - if (!elementAccessToParamName.TryGetValue(eaStr, out string id_name)) - { - // Generate a random suffix - string suffix = Guid.NewGuid().ToString().Substring(0, 5); - string prefix = eaStr.Trim().Replace(".", "_").Replace("[", "_").Replace("]", "_"); - id_name = $"{prefix}_{suffix}"; - elementAccessToParamName[eaStr] = id_name; - } - - return SyntaxFactory.IdentifierName(id_name); - }); - - var localsSet = new HashSet(); - - // 2. For every unique member ref, add a corresponding method param - if (ma_values != null) - { - foreach ((MemberAccessExpressionSyntax maes, JObject value) in memberAccesses.Zip(ma_values)) - { - string node_str = maes.ToString(); - if (!memberAccessToParamName.TryGetValue(node_str, out string id_name)) - { - throw new Exception($"BUG: Expected to find an id name for the member access string: {node_str}"); - } - memberAccessValues[id_name] = value; - AddLocalVariableWithValue(id_name, value); - } - } - - if (id_values != null) - { - foreach ((IdentifierNameSyntax idns, JObject value) in identifiers.Zip(id_values)) - { - AddLocalVariableWithValue(idns.Identifier.Text, value); - } - } - - if (method_values != null) - { - foreach ((InvocationExpressionSyntax ies, JObject value) in methodCall.Zip(method_values)) - { - string node_str = ies.ToString(); - if (!methodCallToParamName.TryGetValue(node_str, out string id_name)) - { - throw new Exception($"BUG: Expected to find an id name for the member access string: {node_str}"); - } - AddLocalVariableWithValue(id_name, value); - } - } - - if (ea_values != null) - { - foreach ((ElementAccessExpressionSyntax eas, JObject value) in elementAccess.Zip(ea_values)) - { - string node_str = eas.ToString(); - if (!elementAccessToParamName.TryGetValue(node_str, out string id_name)) - { - throw new Exception($"BUG: Expected to find an id name for the element access string: {node_str}"); - } - AddLocalVariableWithValue(id_name, value); - } - } - - return syntaxTree.WithRootAndOptions(root, syntaxTree.Options); - - void AddLocalVariableWithValue(string idName, JObject value) - { - if (localsSet.Contains(idName)) - return; - localsSet.Add(idName); - variableDefinitions.Add(ConvertJSToCSharpLocalVariableAssignment(idName, value)); - } - } - - private string ConvertJSToCSharpLocalVariableAssignment(string idName, JToken variable) - { - string typeRet; - object valueRet; - JToken value = variable["value"]; - string type = variable["type"].Value(); - string subType = variable["subtype"]?.Value(); - string objectId = variable["objectId"]?.Value(); - switch (type) - { - case "string": - { - var str = value?.Value(); - str = str.Replace("\"", "\\\""); - valueRet = $"\"{str}\""; - 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().ToLower(); - typeRet = "bool"; - break; - case "object": - valueRet = "Newtonsoft.Json.Linq.JObject.FromObject(new {" - + $"type = \"{type}\"" - + $", description = \"{variable["description"].Value()}\"" - + $", className = \"{variable["className"].Value()}\"" - + (subType != null ? $", subtype = \"{subType}\"" : "") - + (objectId != null ? $", objectId = \"{objectId}\"" : "") - + "})"; - typeRet = "object"; - break; - case "void": - valueRet = "Newtonsoft.Json.Linq.JObject.FromObject(new {" - + $"type = \"object\"," - + $"description = \"object\"," - + $"className = \"object\"," - + $"subtype = \"null\"" - + "})"; - typeRet = "object"; - break; - default: - throw new Exception($"Evaluate of this datatype {type} not implemented yet");//, "Unsupported"); - } - return $"{typeRet} {idName} = {valueRet};"; - } - } - - private static async Task> ResolveMemberAccessExpressions(IEnumerable member_accesses, - MemberReferenceResolver resolver, CancellationToken token) - { - var memberAccessValues = new List(); - foreach (MemberAccessExpressionSyntax maes in member_accesses) - { - string memberAccessString = maes.ToString(); - JObject value = await resolver.Resolve(memberAccessString, token); - if (value == null) - throw new ReturnAsErrorException($"Failed to resolve member access for {memberAccessString}", "ReferenceError"); - - memberAccessValues.Add(value); - } - - return memberAccessValues; - } - - private static async Task> ResolveIdentifiers(IEnumerable identifiers, MemberReferenceResolver resolver, CancellationToken token) - { - var values = new List(); - foreach (IdentifierNameSyntax var in identifiers) - { - JObject value = await resolver.Resolve(var.Identifier.Text, token); - if (value == null) - throw new ReturnAsErrorException($"The name {var.Identifier.Text} does not exist in the current context", "ReferenceError"); - - values.Add(value); - } - - return values; - } - - private static async Task> ResolveMethodCalls(IEnumerable methodCalls, Dictionary memberAccessValues, MemberReferenceResolver resolver, CancellationToken token) - { - var values = new List(); - foreach (InvocationExpressionSyntax methodCall in methodCalls) - { - JObject value = await resolver.Resolve(methodCall, memberAccessValues, token); - if (value == null) - throw new ReturnAsErrorException($"Failed to resolve member access for {methodCall}", "ReferenceError"); - - values.Add(value); - } - return values; - } - - private static async Task> ResolveElementAccess(IEnumerable elementAccesses, Dictionary memberAccessValues, MemberReferenceResolver resolver, CancellationToken token) - { - var values = new List(); - JObject index = null; - foreach (ElementAccessExpressionSyntax elementAccess in elementAccesses.Reverse()) - { - index = await resolver.Resolve(elementAccess, memberAccessValues, index, token); - if (index == null) - throw new ReturnAsErrorException($"Failed to resolve element access for {elementAccess}", "ReferenceError"); - } - values.Add(index); - return values; - } - - internal static async Task CompileAndRunTheExpression(string expression, MemberReferenceResolver resolver, CancellationToken token) - { - expression = expression.Trim(); - if (!expression.StartsWith('(')) - { - expression = "(" + expression + "\n)"; - } - SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(expression + @";", cancellationToken: token); - - SyntaxNode expressionTree = syntaxTree.GetCompilationUnitRoot(token); - if (expressionTree == null) - throw new Exception($"BUG: Unable to evaluate {expression}, could not get expression from the syntax tree"); - FindVariableNMethodCall findVarNMethodCall = new FindVariableNMethodCall(); - findVarNMethodCall.VisitInternal(expressionTree); - // 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) - { - string varName = expressionTree.ToString(); - JObject value = await resolver.Resolve(varName, token); - if (value == null) - throw new ReturnAsErrorException($"Cannot find member named '{varName}'.", "ReferenceError"); - - return value; - } - - IList memberAccessValues = await ResolveMemberAccessExpressions(findVarNMethodCall.memberAccesses, resolver, token); - - // eg. "this.dateTime", " dateTime.TimeOfDay" - if (expressionTree.Kind() == SyntaxKind.SimpleMemberAccessExpression && findVarNMethodCall.memberAccesses.Count == 1) - { - return memberAccessValues[0]; - } - - IList identifierValues = await ResolveIdentifiers(findVarNMethodCall.identifiers, resolver, token); - - syntaxTree = findVarNMethodCall.ReplaceVars(syntaxTree, memberAccessValues, identifierValues, null, null); - - if (findVarNMethodCall.hasMethodCalls) - { - expressionTree = syntaxTree.GetCompilationUnitRoot(token); - - findVarNMethodCall.VisitInternal(expressionTree); - - IList methodValues = await ResolveMethodCalls(findVarNMethodCall.methodCall, findVarNMethodCall.memberAccessValues, resolver, token); - - syntaxTree = findVarNMethodCall.ReplaceVars(syntaxTree, null, null, methodValues, null); - } - - // eg. "elements[0]" - if (findVarNMethodCall.hasElementAccesses) - { - expressionTree = syntaxTree.GetCompilationUnitRoot(token); - - findVarNMethodCall.VisitInternal(expressionTree); - - IList elementAccessValues = await ResolveElementAccess(findVarNMethodCall.elementAccess, findVarNMethodCall.memberAccessValues, resolver, token); - - syntaxTree = findVarNMethodCall.ReplaceVars(syntaxTree, null, null, null, elementAccessValues); - } - - expressionTree = syntaxTree.GetCompilationUnitRoot(token); - if (expressionTree == null) - throw new Exception($"BUG: Unable to evaluate {expression}, could not get expression from the syntax tree"); - - try - { - var newScript = script.ContinueWith( - string.Join("\n", findVarNMethodCall.variableDefinitions) + "\nreturn " + syntaxTree.ToString()); - - var state = await newScript.RunAsync(cancellationToken: token); - return JObject.FromObject(ConvertCSharpToJSType(state.ReturnValue, state.ReturnValue?.GetType())); - } - catch (CompilationErrorException cee) - { - throw new ReturnAsErrorException($"Cannot evaluate '{expression}': {cee.Message}", "CompilationError"); - } - catch (Exception ex) - { - throw new Exception($"Internal Error: Unable to run {expression}, error: {ex.Message}.", ex); - } - } - - private static readonly HashSet NumericTypes = new HashSet - { - typeof(decimal), typeof(byte), typeof(sbyte), - typeof(short), typeof(ushort), - typeof(int), typeof(uint), - typeof(float), typeof(double) - }; - - private static 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 JObject) - return v; - return new { type = "object", value = v, description = v.ToString(), className = type.ToString() }; - } - - } - - internal class ReturnAsErrorException : Exception - { - private Result _error; - public Result Error - { - get - { - _error.Value["exceptionDetails"]["stackTrace"] = StackTrace; - return _error; - } - set { } - } - public ReturnAsErrorException(JObject error) : base(error.ToString()) - => Error = Result.Err(error); - - public ReturnAsErrorException(string message, string className) - : base($"[{className}] {message}") - { - var result = new - { - type = "object", - subtype = "error", - description = message, - className - }; - _error = Result.UserVisibleErr(JObject.FromObject( - new - { - result = result, - exceptionDetails = new - { - exception = result, - stackTrace = StackTrace - } - })); - } - - public override string ToString() => $"Error object: {Error}. {base.ToString()}"; - } -} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/ExpressionEvaluator.cs b/src/mono/wasm/debugger/BrowserDebugProxy/ExpressionEvaluator.cs new file mode 100644 index 00000000000000..c830071d5c9152 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/ExpressionEvaluator.cs @@ -0,0 +1,247 @@ +// 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.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +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.Scripting; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.Scripting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Text.RegularExpressions; +using System.Globalization; +using BrowserDebugProxy; + +namespace Microsoft.WebAssembly.Diagnostics +{ + internal static class ExpressionEvaluator + { + internal static Script script = CSharpScript.Create( + "", + ScriptOptions.Default.WithReferences( + typeof(object).Assembly, + typeof(Enumerable).Assembly, + typeof(JObject).Assembly + )); + + private static async Task> ResolveMemberAccessExpressions(IEnumerable member_accesses, + MemberReferenceResolver resolver, CancellationToken token) + { + var memberAccessValues = new List(); + foreach (MemberAccessExpressionSyntax maes in member_accesses) + { + string memberAccessString = maes.ToString(); + JObject value = await resolver.Resolve(memberAccessString, token); + if (value == null) + throw new ReturnAsErrorException($"Failed to resolve member access for {memberAccessString}", "ReferenceError"); + + memberAccessValues.Add(value); + } + + return memberAccessValues; + } + + private static async Task> ResolveIdentifiers(IEnumerable identifiers, MemberReferenceResolver resolver, CancellationToken token) + { + var values = new List(); + foreach (IdentifierNameSyntax var in identifiers) + { + JObject value = await resolver.Resolve(var.Identifier.Text, token); + if (value == null) + throw new ReturnAsErrorException($"The name {var.Identifier.Text} does not exist in the current context", "ReferenceError"); + + values.Add(value); + } + + return values; + } + + private static async Task> ResolveMethodCalls(IEnumerable methodCalls, Dictionary memberAccessValues, MemberReferenceResolver resolver, CancellationToken token) + { + var values = new List(); + foreach (InvocationExpressionSyntax methodCall in methodCalls) + { + JObject value = await resolver.Resolve(methodCall, memberAccessValues, token); + if (value == null) + throw new ReturnAsErrorException($"Failed to resolve member access for {methodCall}", "ReferenceError"); + + values.Add(value); + } + return values; + } + + private static async Task> ResolveElementAccess(IEnumerable elementAccesses, Dictionary memberAccessValues, MemberReferenceResolver resolver, CancellationToken token) + { + var values = new List(); + JObject index = null; + foreach (ElementAccessExpressionSyntax elementAccess in elementAccesses.Reverse()) + { + index = await resolver.Resolve(elementAccess, memberAccessValues, index, token); + if (index == null) + throw new ReturnAsErrorException($"Failed to resolve element access for {elementAccess}", "ReferenceError"); + } + values.Add(index); + return values; + } + + internal static async Task CompileAndRunTheExpression(string expression, MemberReferenceResolver resolver, CancellationToken token) + { + expression = expression.Trim(); + if (!expression.StartsWith('(')) + { + expression = "(" + expression + "\n)"; + } + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(expression + @";", cancellationToken: token); + + SyntaxNode expressionTree = syntaxTree.GetCompilationUnitRoot(token); + if (expressionTree == null) + throw new Exception($"BUG: Unable to evaluate {expression}, could not get expression from the syntax tree"); + ExpressionSyntaxReplacer replacer = new ExpressionSyntaxReplacer(); + replacer.VisitInternal(expressionTree); + // this fails with `"a)"` + // because the code becomes: return (a)); + // and the returned expression from GetExpressionFromSyntaxTree is `a`! + if (expressionTree.Kind() == SyntaxKind.IdentifierName || expressionTree.Kind() == SyntaxKind.ThisExpression) + { + string varName = expressionTree.ToString(); + JObject value = await resolver.Resolve(varName, token); + if (value == null) + throw new ReturnAsErrorException($"Cannot find member named '{varName}'.", "ReferenceError"); + + return value; + } + + IList memberAccessValues = await ResolveMemberAccessExpressions(replacer.memberAccesses, resolver, token); + + // eg. "this.dateTime", " dateTime.TimeOfDay" + if (expressionTree.Kind() == SyntaxKind.SimpleMemberAccessExpression && replacer.memberAccesses.Count == 1) + { + return memberAccessValues[0]; + } + + IList identifierValues = await ResolveIdentifiers(replacer.identifiers, resolver, token); + + syntaxTree = replacer.ReplaceVars(syntaxTree, memberAccessValues, identifierValues, null, null); + + if (replacer.hasMethodCalls) + { + expressionTree = syntaxTree.GetCompilationUnitRoot(token); + + replacer.VisitInternal(expressionTree); + + IList methodValues = await ResolveMethodCalls(replacer.methodCall, replacer.memberAccessValues, resolver, token); + + syntaxTree = replacer.ReplaceVars(syntaxTree, null, null, methodValues, null); + } + + // eg. "elements[0]" + if (replacer.hasElementAccesses) + { + expressionTree = syntaxTree.GetCompilationUnitRoot(token); + + replacer.VisitInternal(expressionTree); + + IList elementAccessValues = await ResolveElementAccess(replacer.elementAccess, replacer.memberAccessValues, resolver, token); + + syntaxTree = replacer.ReplaceVars(syntaxTree, null, null, null, elementAccessValues); + } + + expressionTree = syntaxTree.GetCompilationUnitRoot(token); + if (expressionTree == null) + throw new Exception($"BUG: Unable to evaluate {expression}, could not get expression from the syntax tree"); + + try + { + var newScript = script.ContinueWith( + string.Join("\n", replacer.variableDefinitions) + "\nreturn " + syntaxTree.ToString()); + + var state = await newScript.RunAsync(cancellationToken: token); + return JObject.FromObject(ConvertCSharpToJSType(state.ReturnValue, state.ReturnValue?.GetType())); + } + catch (CompilationErrorException cee) + { + throw new ReturnAsErrorException($"Cannot evaluate '{expression}': {cee.Message}", "CompilationError"); + } + catch (Exception ex) + { + throw new Exception($"Internal Error: Unable to run {expression}, error: {ex.Message}.", ex); + } + } + + private static readonly HashSet NumericTypes = new HashSet + { + typeof(decimal), typeof(byte), typeof(sbyte), + typeof(short), typeof(ushort), + typeof(int), typeof(uint), + typeof(float), typeof(double) + }; + + private static 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 JObject) + return v; + if (v is bool) + return new { type = "boolean", value = v, description = v.ToString().ToLower(), className = type.ToString() }; + return new { type = "object", value = v, description = v.ToString(), className = type.ToString() }; + } + + } + + internal class ReturnAsErrorException : Exception + { + private Result _error; + public Result Error + { + get + { + _error.Value["exceptionDetails"]["stackTrace"] = StackTrace; + return _error; + } + set { } + } + public ReturnAsErrorException(JObject error) : base(error.ToString()) + => Error = Result.Err(error); + + public ReturnAsErrorException(string message, string className) + : base($"[{className}] {message}") + { + var result = new + { + type = "object", + subtype = "error", + description = message, + className + }; + _error = Result.UserVisibleErr(JObject.FromObject( + new + { + result = result, + exceptionDetails = new + { + exception = result, + stackTrace = StackTrace + } + })); + } + + public override string ToString() => $"Error object: {Error}. {base.ToString()}"; + } +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/ExpressionSyntaxReplacer.cs b/src/mono/wasm/debugger/BrowserDebugProxy/ExpressionSyntaxReplacer.cs new file mode 100644 index 00000000000000..8f73cedfaa063b --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/ExpressionSyntaxReplacer.cs @@ -0,0 +1,255 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.WebAssembly.Diagnostics; +using Newtonsoft.Json.Linq; + +namespace BrowserDebugProxy +{ + public class ExpressionSyntaxReplacer : CSharpSyntaxWalker + { + private static Regex regexForReplaceVarName = new Regex(@"[^A-Za-z0-9_]", RegexOptions.Singleline); + public List identifiers = new List(); + public List methodCall = new List(); + public List memberAccesses = new List(); + public List elementAccess = new List(); + public List argValues = new List(); + public Dictionary memberAccessValues = new Dictionary(); + private int visitCount; + public bool hasMethodCalls; + public bool hasElementAccesses; + internal List variableDefinitions = new List(); + + public void VisitInternal(SyntaxNode node) + { + Visit(node); + visitCount++; + } + public override void Visit(SyntaxNode node) + { + // TODO: PointerMemberAccessExpression + if (visitCount == 0) + { + if (node is MemberAccessExpressionSyntax maes + && node.Kind() == SyntaxKind.SimpleMemberAccessExpression + && !(node.Parent is MemberAccessExpressionSyntax) + && !(node.Parent is InvocationExpressionSyntax) + && !(node.Parent is ElementAccessExpressionSyntax)) + { + memberAccesses.Add(maes); + } + + if (node is IdentifierNameSyntax identifier + && !(identifier.Parent is MemberAccessExpressionSyntax) + && !(identifier.Parent is InvocationExpressionSyntax) + && !(node.Parent is ElementAccessExpressionSyntax) + && !identifiers.Any(x => x.Identifier.Text == identifier.Identifier.Text)) + { + identifiers.Add(identifier); + } + } + + if (node is InvocationExpressionSyntax) + { + if (visitCount == 1) + methodCall.Add(node as InvocationExpressionSyntax); + hasMethodCalls = true; + } + + if (node is ElementAccessExpressionSyntax) + { + if (visitCount == 1) + elementAccess.Add(node as ElementAccessExpressionSyntax); + hasElementAccesses = true; + } + + if (node is AssignmentExpressionSyntax) + throw new Exception("Assignment is not implemented yet"); + base.Visit(node); + } + + public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable ma_values, IEnumerable id_values, IEnumerable method_values, IEnumerable ea_values) + { + var memberAccessToParamName = new Dictionary(); + var methodCallToParamName = new Dictionary(); + var elementAccessToParamName = new Dictionary(); + + CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot(); + + // 1. Replace all this.a occurrences with this_a_ABDE + root = root.ReplaceNodes(memberAccesses, (maes, _) => + { + string ma_str = maes.ToString(); + if (!memberAccessToParamName.TryGetValue(ma_str, out string id_name)) + { + // Generate a random suffix + string suffix = Guid.NewGuid().ToString().Substring(0, 5); + string prefix = regexForReplaceVarName.Replace(ma_str, "_"); + id_name = $"{prefix}_{suffix}"; + + memberAccessToParamName[ma_str] = id_name; + } + + return SyntaxFactory.IdentifierName(id_name); + }); + + // 1.1 Replace all this.a() occurrences with this_a_ABDE + root = root.ReplaceNodes(methodCall, (m, _) => + { + string iesStr = m.ToString(); + if (!methodCallToParamName.TryGetValue(iesStr, out string id_name)) + { + // Generate a random suffix + string suffix = Guid.NewGuid().ToString().Substring(0, 5); + string prefix = regexForReplaceVarName.Replace(iesStr, "_"); + id_name = $"{prefix}_{suffix}"; + methodCallToParamName[iesStr] = id_name; + } + + return SyntaxFactory.IdentifierName(id_name); + }); + + // 1.2 Replace all this.a[x] occurrences with this_a_ABDE + root = root.ReplaceNodes(elementAccess, (ea, _) => + { + string eaStr = ea.ToString(); + if (!elementAccessToParamName.TryGetValue(eaStr, out string id_name)) + { + // Generate a random suffix + string suffix = Guid.NewGuid().ToString().Substring(0, 5); + string prefix = eaStr.Trim().Replace(".", "_").Replace("[", "_").Replace("]", "_"); + id_name = $"{prefix}_{suffix}"; + elementAccessToParamName[eaStr] = id_name; + } + + return SyntaxFactory.IdentifierName(id_name); + }); + + var localsSet = new HashSet(); + + // 2. For every unique member ref, add a corresponding method param + if (ma_values != null) + { + foreach ((MemberAccessExpressionSyntax maes, JObject value) in memberAccesses.Zip(ma_values)) + { + string node_str = maes.ToString(); + if (!memberAccessToParamName.TryGetValue(node_str, out string id_name)) + { + throw new Exception($"BUG: Expected to find an id name for the member access string: {node_str}"); + } + memberAccessValues[id_name] = value; + AddLocalVariableWithValue(id_name, value); + } + } + + if (id_values != null) + { + foreach ((IdentifierNameSyntax idns, JObject value) in identifiers.Zip(id_values)) + { + AddLocalVariableWithValue(idns.Identifier.Text, value); + } + } + + if (method_values != null) + { + foreach ((InvocationExpressionSyntax ies, JObject value) in methodCall.Zip(method_values)) + { + string node_str = ies.ToString(); + if (!methodCallToParamName.TryGetValue(node_str, out string id_name)) + { + throw new Exception($"BUG: Expected to find an id name for the member access string: {node_str}"); + } + AddLocalVariableWithValue(id_name, value); + } + } + + if (ea_values != null) + { + foreach ((ElementAccessExpressionSyntax eas, JObject value) in elementAccess.Zip(ea_values)) + { + string node_str = eas.ToString(); + if (!elementAccessToParamName.TryGetValue(node_str, out string id_name)) + { + throw new Exception($"BUG: Expected to find an id name for the element access string: {node_str}"); + } + AddLocalVariableWithValue(id_name, value); + } + } + + return syntaxTree.WithRootAndOptions(root, syntaxTree.Options); + + void AddLocalVariableWithValue(string idName, JObject value) + { + if (localsSet.Contains(idName)) + return; + localsSet.Add(idName); + variableDefinitions.Add(ConvertJSToCSharpLocalVariableAssignment(idName, value)); + } + } + + private string ConvertJSToCSharpLocalVariableAssignment(string idName, JToken variable) + { + string typeRet; + object valueRet; + JToken value = variable["value"]; + string type = variable["type"].Value(); + string subType = variable["subtype"]?.Value(); + string objectId = variable["objectId"]?.Value(); + switch (type) + { + case "string": + { + var str = value?.Value(); + str = str.Replace("\"", "\\\""); + valueRet = $"\"{str}\""; + 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().ToLower(); + typeRet = "bool"; + break; + case "object": + valueRet = "Newtonsoft.Json.Linq.JObject.FromObject(new {" + + $"type = \"{type}\"" + + $", description = \"{variable["description"].Value()}\"" + + $", className = \"{variable["className"].Value()}\"" + + (subType != null ? $", subtype = \"{subType}\"" : "") + + (objectId != null ? $", objectId = \"{objectId}\"" : "") + + "})"; + typeRet = "object"; + break; + case "void": + valueRet = "Newtonsoft.Json.Linq.JObject.FromObject(new {" + + $"type = \"object\"," + + $"description = \"object\"," + + $"className = \"object\"," + + $"subtype = \"null\"" + + "})"; + typeRet = "object"; + break; + default: + throw new Exception($"Evaluate of this datatype {type} not implemented yet");//, "Unsupported"); + } + return $"{typeRet} {idName} = {valueRet};"; + } + } +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs index 581f7ef5cf6ba8..fe53990dcc002b 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -798,7 +798,7 @@ private async Task EvaluateCondition(SessionId sessionId, ExecutionContext var resolver = new MemberReferenceResolver(this, context, sessionId, mono_frame.Id, logger); JObject retValue = await resolver.Resolve(condition, token); if (retValue == null) - retValue = await EvaluateExpression.CompileAndRunTheExpression(condition, resolver, token); + retValue = await ExpressionEvaluator.CompileAndRunTheExpression(condition, resolver, token); if (retValue?["value"]?.Type == JTokenType.Boolean || retValue?["value"]?.Type == JTokenType.Integer || retValue?["value"]?.Type == JTokenType.Float) { @@ -1255,7 +1255,7 @@ private async Task OnEvaluateOnCallFrame(MessageId msg_id, int scopeId, st JObject retValue = await resolver.Resolve(expression, token); if (retValue == null) { - retValue = await EvaluateExpression.CompileAndRunTheExpression(expression, resolver, token); + retValue = await ExpressionEvaluator.CompileAndRunTheExpression(expression, resolver, token); } if (retValue != null) diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs index be8a074b35ec7a..820fdca6575623 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs @@ -1415,7 +1415,7 @@ public async Task GetValueFromDebuggerDisplayAttribute(int objectId, int expr = "$\"" + dispAttrStr + "\""; JObject retValue = await resolver.Resolve(expr, token); if (retValue == null) - retValue = await EvaluateExpression.CompileAndRunTheExpression(expr, resolver, token); + retValue = await ExpressionEvaluator.CompileAndRunTheExpression(expr, resolver, token); return retValue?["value"]?.Value(); } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs index 3e3071a43988b8..4767864d61fd7c 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs @@ -1084,9 +1084,9 @@ await EvaluateOnCallFrameAndCheck(id, ("test.GetSingleNullable()", JObject.FromObject( new { type = "number", value = 1.23, description = "1.23" })), ("test.GetDoubleNullable()", JObject.FromObject( new { type = "number", value = 1.23, description = "1.23" })), - ("test.GetBool()", JObject.FromObject( new { type = "object", value = true, description = "True", className = "System.Boolean" })), - ("test.GetBoolNullable()", JObject.FromObject( new { type = "object", value = true, description = "True", className = "System.Boolean" })), - ("test.GetNull()", JObject.FromObject( new { type = "object", value = true, description = "True", className = "System.Boolean" })), + ("test.GetBool()", TBool(true)), + ("test.GetBoolNullable()", TBool(true)), + ("test.GetNull()", TBool(true)), ("test.GetDefaultAndRequiredParam(2)", TNumber(5)), ("test.GetDefaultAndRequiredParam(3, 2)", TNumber(5)),