Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for #60340 #60863

Merged
merged 12 commits into from
Nov 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 70 additions & 4 deletions src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ private class FindVariableNMethodCall : CSharpSyntaxWalker
public List<IdentifierNameSyntax> identifiers = new List<IdentifierNameSyntax>();
public List<InvocationExpressionSyntax> methodCall = new List<InvocationExpressionSyntax>();
public List<MemberAccessExpressionSyntax> memberAccesses = new List<MemberAccessExpressionSyntax>();
public List<ElementAccessExpressionSyntax> elementAccess = new List<ElementAccessExpressionSyntax>();
public List<object> argValues = new List<object>();
public Dictionary<string, JObject> memberAccessValues = new Dictionary<string, JObject>();
private int visitCount;
public bool hasMethodCalls;
public bool hasElementAccesses;

public void VisitInternal(SyntaxNode node)
{
Expand All @@ -44,14 +46,16 @@ public override void Visit(SyntaxNode node)
if (node is MemberAccessExpressionSyntax maes
&& node.Kind() == SyntaxKind.SimpleMemberAccessExpression
&& !(node.Parent is MemberAccessExpressionSyntax)
&& !(node.Parent is InvocationExpressionSyntax))
&& !(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);
Expand All @@ -65,15 +69,23 @@ public override void Visit(SyntaxNode node)
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<JObject> ma_values, IEnumerable<JObject> id_values, IEnumerable<JObject> method_values)
public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable<JObject> ma_values, IEnumerable<JObject> id_values, IEnumerable<JObject> method_values, IEnumerable<JObject> ea_values)
{
var memberAccessToParamName = new Dictionary<string, string>();
var methodCallToParamName = new Dictionary<string, string>();
var elementAccessToParamName = new Dictionary<string, string>();

CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot();

Expand Down Expand Up @@ -110,6 +122,22 @@ public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable<JObject> ma_val
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 paramsSet = new HashSet<string>();

// 2. For every unique member ref, add a corresponding method param
Expand Down Expand Up @@ -148,6 +176,18 @@ public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable<JObject> ma_val
}
}

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}");
}
root = UpdateWithNewMethodParam(root, id_name, value);
}
}

return syntaxTree.WithRootAndOptions(root, syntaxTree.Options);

Expand Down Expand Up @@ -281,6 +321,20 @@ private static async Task<IList<JObject>> ResolveMethodCalls(IEnumerable<Invocat
return values;
}

private static async Task<IList<JObject>> ResolveElementAccess(IEnumerable<ElementAccessExpressionSyntax> elementAccesses, Dictionary<string, JObject> memberAccessValues, MemberReferenceResolver resolver, CancellationToken token)
{
var values = new List<JObject>();
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;
}

[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file",
Justification = "Suppressing the warning until gets fixed, see https://github.com/dotnet/runtime/issues/51202")]
internal static async Task<JObject> CompileAndRunTheExpression(string expression, MemberReferenceResolver resolver, CancellationToken token)
Expand Down Expand Up @@ -330,7 +384,7 @@ public static object Evaluate()

IList<JObject> identifierValues = await ResolveIdentifiers(findVarNMethodCall.identifiers, resolver, token);

syntaxTree = findVarNMethodCall.ReplaceVars(syntaxTree, memberAccessValues, identifierValues, null);
syntaxTree = findVarNMethodCall.ReplaceVars(syntaxTree, memberAccessValues, identifierValues, null, null);

if (findVarNMethodCall.hasMethodCalls)
{
Expand All @@ -340,7 +394,19 @@ public static object Evaluate()

IList<JObject> methodValues = await ResolveMethodCalls(findVarNMethodCall.methodCall, findVarNMethodCall.memberAccessValues, resolver, token);

syntaxTree = findVarNMethodCall.ReplaceVars(syntaxTree, null, null, methodValues);
syntaxTree = findVarNMethodCall.ReplaceVars(syntaxTree, null, null, methodValues, null);
}

// eg. "elements[0]"
if (findVarNMethodCall.hasElementAccesses)
{
expressionTree = GetExpressionFromSyntaxTree(syntaxTree);

findVarNMethodCall.VisitInternal(expressionTree);

IList<JObject> elementAccessValues = await ResolveElementAccess(findVarNMethodCall.elementAccess, findVarNMethodCall.memberAccessValues, resolver, token);

syntaxTree = findVarNMethodCall.ReplaceVars(syntaxTree, null, null, null, elementAccessValues);
}

expressionTree = GetExpressionFromSyntaxTree(syntaxTree);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,97 @@ public async Task<JObject> Resolve(string varName, CancellationToken token)
return rootObject;
}

public async Task<JObject> Resolve(ElementAccessExpressionSyntax elementAccess, Dictionary<string, JObject> memberAccessValues, JObject indexObject, CancellationToken token)
{
try
{
JObject rootObject = null;
string elementAccessStrExpression = elementAccess.Expression.ToString();
rootObject = await Resolve(elementAccessStrExpression, token);
if (rootObject == null)
{
rootObject = indexObject;
indexObject = null;
}
if (rootObject != null)
{
string elementIdxStr;
int elementIdx = 0;
// x[1] or x[a] or x[a.b]
if (indexObject == null)
{
if (elementAccess.ArgumentList != null)
{
var commandParamsObj = new MemoryStream();
var commandParamsObjWriter = new MonoBinaryWriter(commandParamsObj);
foreach (var arg in elementAccess.ArgumentList.Arguments)
{
// e.g. x[1]
if (arg.Expression is LiteralExpressionSyntax)
{
var argParm = arg.Expression as LiteralExpressionSyntax;
elementIdxStr = argParm.ToString();
int.TryParse(elementIdxStr, out elementIdx);
}

// e.g. x[a] or x[a.b]
if (arg.Expression is IdentifierNameSyntax)
{
var argParm = arg.Expression as IdentifierNameSyntax;

// x[a.b]
memberAccessValues.TryGetValue(argParm.Identifier.Text, out indexObject);

// x[a]
if (indexObject == null)
{
indexObject = await Resolve(argParm.Identifier.Text, token);
}
elementIdxStr = indexObject["value"].ToString();
int.TryParse(elementIdxStr, out elementIdx);
}
}
}
}
// e.g. x[a[0]], x[a[b[1]]] etc.
else
{
elementIdxStr = indexObject["value"].ToString();
int.TryParse(elementIdxStr, out elementIdx);
}
if (elementIdx >= 0)
{
DotnetObjectId.TryParse(rootObject?["objectId"]?.Value<string>(), out DotnetObjectId objectId);
switch (objectId.Scheme)
{
case "array":
rootObject["value"] = await sdbHelper.GetArrayValues(sessionId, int.Parse(objectId.Value), token);
return (JObject)rootObject["value"][elementIdx]["value"];
case "object":
var typeIds = await sdbHelper.GetTypeIdFromObject(sessionId, int.Parse(objectId.Value), true, token);
int methodId = await sdbHelper.GetMethodIdByName(sessionId, typeIds[0], "ToArray", token);
var commandParamsObj = new MemoryStream();
var commandParamsObjWriter = new MonoBinaryWriter(commandParamsObj);
commandParamsObjWriter.WriteObj(objectId, sdbHelper);
var toArrayRetMethod = await sdbHelper.InvokeMethod(sessionId, commandParamsObj.ToArray(), methodId, elementAccess.Expression.ToString(), token);
rootObject = await GetValueFromObject(toArrayRetMethod, token);
DotnetObjectId.TryParse(rootObject?["objectId"]?.Value<string>(), out DotnetObjectId arrayObjectId);
rootObject["value"] = await sdbHelper.GetArrayValues(sessionId, int.Parse(arrayObjectId.Value), token);
return (JObject)rootObject["value"][elementIdx]["value"];
default:
throw new InvalidOperationException($"Cannot apply indexing with [] to an expression of type '{objectId.Scheme}'");
}
}
}
return null;
}
catch (Exception ex)
{
var e = ex;
throw new Exception($"Unable to evaluate method '{elementAccess}'");
}
}

public async Task<JObject> Resolve(InvocationExpressionSyntax method, Dictionary<string, JObject> memberAccessValues, CancellationToken token)
{
var methodName = "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,97 @@ await EvaluateOnCallFrameAndCheck(id,
("this.CallMethodWithObj(this.objToTest)", TNumber(10)));
});

[Fact]
public async Task EvaluateExpressionsWithElementAccessByConstant() => await CheckInspectLocalsAtBreakpointSite(
"DebuggerTests.EvaluateLocalsWithElementAccessTests", "EvaluateLocals", 5, "EvaluateLocals",
"window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithElementAccessTests:EvaluateLocals'); })",
wait_for_event_fn: async (pause_location) =>
{
var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();

await EvaluateOnCallFrameAndCheck(id,
("f.numList[0]", TNumber(1)),
("f.textList[1]", TString("2")),
("f.numArray[1]", TNumber(2)),
("f.textArray[0]", TString("1")));
});

[Fact]
public async Task EvaluateExpressionsWithElementAccessByLocalVariable() => await CheckInspectLocalsAtBreakpointSite(
"DebuggerTests.EvaluateLocalsWithElementAccessTests", "EvaluateLocals", 5, "EvaluateLocals",
"window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithElementAccessTests:EvaluateLocals'); })",
wait_for_event_fn: async (pause_location) =>
{
var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();

await EvaluateOnCallFrameAndCheck(id,
("f.numList[i]", TNumber(1)),
("f.textList[j]", TString("2")),
("f.numArray[j]", TNumber(2)),
("f.textArray[i]", TString("1")));

});

[Fact]
public async Task EvaluateExpressionsWithElementAccessByMemberVariables() => await CheckInspectLocalsAtBreakpointSite(
"DebuggerTests.EvaluateLocalsWithElementAccessTests", "EvaluateLocals", 5, "EvaluateLocals",
"window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithElementAccessTests:EvaluateLocals'); })",
wait_for_event_fn: async (pause_location) =>
{
var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();

await EvaluateOnCallFrameAndCheck(id,
("f.idx0", TNumber(0)),
("f.idx1", TNumber(1)),
("f.numList[f.idx0]", TNumber(1)),
("f.textList[f.idx1]", TString("2")),
("f.numArray[f.idx1]", TNumber(2)),
("f.textArray[f.idx0]", TString("1")));

});

[Fact]
public async Task EvaluateExpressionsWithElementAccessNested() => await CheckInspectLocalsAtBreakpointSite(
"DebuggerTests.EvaluateLocalsWithElementAccessTests", "EvaluateLocals", 5, "EvaluateLocals",
"window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithElementAccessTests:EvaluateLocals'); })",
wait_for_event_fn: async (pause_location) =>
{
var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();

await EvaluateOnCallFrameAndCheck(id,
("f.idx0", TNumber(0)),
("f.numList[f.numList[f.idx0]]", TNumber(2)),
("f.textList[f.numList[f.idx0]]", TString("2")),
("f.numArray[f.numArray[f.idx0]]", TNumber(2)),
("f.textArray[f.numArray[f.idx0]]", TString("2")));

});

[Fact]
public async Task EvaluateExpressionsWithElementAccessMultidimentional() => await CheckInspectLocalsAtBreakpointSite(
"DebuggerTests.EvaluateLocalsWithElementAccessTests", "EvaluateLocals", 5, "EvaluateLocals",
"window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithElementAccessTests:EvaluateLocals'); })",
wait_for_event_fn: async (pause_location) =>
{
var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();

await EvaluateOnCallFrameAndCheck(id,
("j", TNumber(1)),
("f.idx1", TNumber(1)),
("f.numArrayOfArrays[1][1]", TNumber(2)),
("f.numArrayOfArrays[j][j]", TNumber(2)),
("f.numArrayOfArrays[f.idx1][f.idx1]", TNumber(2)),
("f.numListOfLists[1][1]", TNumber(2)),
("f.numListOfLists[j][j]", TNumber(2)),
("f.numListOfLists[f.idx1][f.idx1]", TNumber(2)),
("f.textArrayOfArrays[1][1]", TString("2")),
("f.textArrayOfArrays[j][j]", TString("2")),
("f.textArrayOfArrays[f.idx1][f.idx1]", TString("2")),
("f.textListOfLists[1][1]", TString("2")),
("f.textListOfLists[j][j]", TString("2")),
("f.textListOfLists[f.idx1][f.idx1]", TString("2")));

});

[Fact]
public async Task EvaluateSimpleMethodCallsCheckChangedValue() => await CheckInspectLocalsAtBreakpointSite(
Expand Down
Loading