Skip to content

Commit

Permalink
Fix for #60340 (#60863)
Browse files Browse the repository at this point in the history
* Fix for #60340

* Fixes element access for a constant index.

* Undo changes made based on #60873, that were not intended to be commited.

* Removed whitespaces.

* Fixed element access by local variables, added tests for element access by object member variables.

* Element access by indexing with simple object members is fixed.

* Nested element access is fixed, e.g. a[a[a[0]]].

* Added tests for nested element access. Reverted an unintentional change in EvaluateSimpleMethodCallsCheckChangedValue test.

* Change test name to enable running test batch on calling ".EvaluateExpressionsWithElementAccess" prefix.

* Fix ElementAccessByMemberVariables that was failing after previous changes.

* Removed unused namespace import. Regexes are not needed in the new approach.

* Added implementatio of evaluation for multidimentional indexing.
  • Loading branch information
ilonatommy authored Nov 1, 2021
1 parent a5eacee commit 1711db7
Show file tree
Hide file tree
Showing 4 changed files with 292 additions and 4 deletions.
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

0 comments on commit 1711db7

Please sign in to comment.