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

[wasm][debugger] Implement support for null-conditional operators in simple expressions #69307

Merged
merged 16 commits into from
Jun 8, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -382,13 +382,6 @@ private static async Task<IList<JObject>> ResolveElementAccess(IEnumerable<Eleme
return values;
}

internal static string RemoveNullSuppression(string expression)
{
expression = expression.Trim();
expression = expression.Replace("!.", ".").Replace("![", "[").Replace("!(", "(");
return expression;
}

internal static async Task<JObject> CompileAndRunTheExpression(
string expression, MemberReferenceResolver resolver, ILogger logger, CancellationToken token)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public async Task<JObject> GetValueFromObject(JToken objRet, CancellationToken t
{
ArraySegment<string> remaining = null;
if (i < parts.Count - 1)
remaining = parts[(i + 1)..];
remaining = parts[i..];

return (memberObject, remaining);
}
Expand Down Expand Up @@ -190,22 +190,14 @@ public async Task<JObject> Resolve(string varName, CancellationToken token)
if (scopeCache.ObjectFields.TryGetValue(varName, out JObject valueRet))
return await GetValueFromObject(valueRet, token);

ArraySegment<string> parts = new ArraySegment<string>(varName.Split(".", StringSplitOptions.TrimEntries));
if (parts.Count == 0 || string.IsNullOrEmpty(parts[0]))
string[] parts = varName.Split(".", StringSplitOptions.TrimEntries);
ilonatommy marked this conversation as resolved.
Show resolved Hide resolved
if (parts.Length == 0 || string.IsNullOrEmpty(parts[0]))
throw new ReturnAsErrorException($"Failed to resolve expression: {varName}", "ReferenceError");

JObject retObject = await ResolveAsLocalOrThisMember(parts[0]);
bool hasNullCondition = parts[0][^1] == '?';
if (retObject != null && parts.Count > 1)
{
// cannot resolve instance member on a primitive type,
// try compiling to check if it's not a method on primitive
var typeName = retObject["className"]?.Value<string>();
if (MonoSDBHelper.IsPrimitiveType(typeName))
return null;

retObject = await ResolveAsInstanceMember(parts[1..], retObject, hasNullCondition);
}
bool throwOnNullReference = parts[0][^1] != '?';
if (retObject != null && parts.Length > 1)
retObject = await ResolveAsInstanceMember(parts, retObject, throwOnNullReference);

if (retObject == null)
{
Expand All @@ -219,7 +211,7 @@ public async Task<JObject> Resolve(string varName, CancellationToken token)
}
else
{
retObject = await ResolveAsInstanceMember(remaining, retObject, hasNullCondition);
retObject = await ResolveAsInstanceMember(remaining, retObject, throwOnNullReference);
}
}
}
Expand All @@ -238,7 +230,7 @@ async Task<JObject> ResolveAsLocalOrThisMember(string name)
}

// remove null-condition, otherwise TryGet by name fails
if (name[^1] == '?')
if (name[^1] == '?' || name[^1] == '!')
name = name.Remove(name.Length - 1);

if (scopeCache.Locals.TryGetValue(name, out JObject obj))
Expand All @@ -264,24 +256,27 @@ async Task<JObject> ResolveAsLocalOrThisMember(string name)
return null;
}

async Task<JObject> ResolveAsInstanceMember(ArraySegment<string> parts, JObject baseObject, bool hasNullCondition)
async Task<JObject> ResolveAsInstanceMember(ArraySegment<string> parts, JObject baseObject, bool throwOnNullReference)
{
// if the prevPart has "?" at the end: true
bool hasResolvedObjNullCondition = hasNullCondition;
bool isPrevPartNull = false;
JObject resolvedObject = baseObject;
for (int i = 0; i < parts.Count; i++)
// parts[0] - name of baseObject
for (int i = 1; i < parts.Count; i++)
{
string partTrimmed = parts[i];
if (partTrimmed.Length == 0)
string part = parts[i];
if (part.Length == 0)
return null;

if (part[^1] == '!')
part = part.Remove(part.Length - 1);

bool hasCurrentPartNullCondition = part[^1] == '?';
radical marked this conversation as resolved.
Show resolved Hide resolved

// current value of resolvedObject is on parts[i - 1]
if (isPrevPartNull || resolvedObject.IsNullValuedObject())
if (resolvedObject.IsNullValuedObject())
{
// trying null.$member
if (!hasResolvedObjNullCondition)
throw new ReturnAsErrorException($"Cannot access member \"{partTrimmed}\" of a null-valued object.", "ReferenceError");
if (throwOnNullReference)
throw new ReturnAsErrorException($"Expression threw NullReferenceException trying to access \"{part}\" on a null-valued object.", "ReferenceError");

if (i == parts.Count - 1)
{
Expand All @@ -290,17 +285,18 @@ async Task<JObject> ResolveAsInstanceMember(ArraySegment<string> parts, JObject
// so the class/description of object are not of the last part
return resolvedObject;
}
// else: trying null?.$member... - allowed operation
isPrevPartNull = true;
}

bool hasCurrentPartNullCondition = partTrimmed.Last() == '?';
hasResolvedObjNullCondition = hasCurrentPartNullCondition;
// check if null condition is correctly applied: should we throw or return null-object
throwOnNullReference = !hasCurrentPartNullCondition;
continue;
}

// cannot resolve the members but can check if null condition is
// correctly applied and if we should throw or return null-object
if (!DotnetObjectId.TryParse(resolvedObject?["objectId"]?.Value<string>(), out DotnetObjectId objectId))
continue;
{
if (!throwOnNullReference)
throw new ReturnAsErrorException($"Operation '?' not allowed on primitive type - '{parts[i - 1]}'", "ReferenceError");
throw new ReturnAsErrorException($"Cannot find member '{part}' on a primitive type", "ReferenceError");
}

var args = JObject.FromObject(new { forDebuggerDisplayAttribute = true });
ilonatommy marked this conversation as resolved.
Show resolved Hide resolved
ValueOrError<GetMembersResult> valueOrError = await proxy.RuntimeGetObjectMembers(sessionId, objectId, args, token);
Expand All @@ -311,15 +307,15 @@ async Task<JObject> ResolveAsInstanceMember(ArraySegment<string> parts, JObject
}

if (hasCurrentPartNullCondition)
ilonatommy marked this conversation as resolved.
Show resolved Hide resolved
partTrimmed = partTrimmed.Remove(partTrimmed.Length - 1);
JToken objRet = valueOrError.Value.FirstOrDefault(objPropAttr => objPropAttr["name"]?.Value<string>() == partTrimmed);
part = part.Remove(part.Length - 1);
ilonatommy marked this conversation as resolved.
Show resolved Hide resolved
JToken objRet = valueOrError.Value.FirstOrDefault(objPropAttr => objPropAttr["name"]?.Value<string>() == part);
if (objRet == null)
return null;

resolvedObject = await GetValueFromObject(objRet, token);
if (resolvedObject == null)
return null;

throwOnNullReference = !hasCurrentPartNullCondition;
}
return resolvedObject;
}
Expand Down
1 change: 0 additions & 1 deletion src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1318,7 +1318,6 @@ private async Task<bool> OnEvaluateOnCallFrame(MessageId msg_id, int scopeId, st

var resolver = new MemberReferenceResolver(this, context, msg_id, scopeId, logger);

expression = ExpressionEvaluator.RemoveNullSuppression(expression);
JObject retValue = await resolver.Resolve(expression, token);
if (retValue == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1163,11 +1163,11 @@ await EvaluateOnCallFrameAndCheck(id,
("list?.Count", TNumber(1)),
("listNull", TObject("System.Collections.Generic.List<int>", is_null: true)),
("listNull?.Count", TObject("System.Collections.Generic.List<int>", is_null: true)),
ilonatommy marked this conversation as resolved.
Show resolved Hide resolved
("tc?.memberList?.Count", TNumber(2)),
("tc!.memberList?.Count", TNumber(2)),
("tc?.memberListNull?.Count", TObject("System.Collections.Generic.List<int>", is_null: true)),
("tc.memberListNull?.Count", TObject("System.Collections.Generic.List<int>", is_null: true)),
("tcNull?.memberListNull?.Count", TObject("DebuggerTests.EvaluateNullableProperties.TestClass", is_null: true)));
("tc?.MemberList?.Count", TNumber(2)),
("tc!.MemberList?.Count", TNumber(2)),
("tc?.MemberListNull?.Count", TObject("System.Collections.Generic.List<int>", is_null: true)),
("tc.MemberListNull?.Count", TObject("System.Collections.Generic.List<int>", is_null: true)),
("tcNull?.MemberListNull?.Count", TObject("DebuggerTests.EvaluateNullableProperties.TestClass", is_null: true)));
});

[ConditionalFact(nameof(RunningOnChrome))]
Expand All @@ -1177,27 +1177,30 @@ public async Task EvaluateNullObjectPropertiesNegative() => await CheckInspectLo
wait_for_event_fn: async (pause_location) =>
{
var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
var (_, res) = await EvaluateOnCallFrame(id, "listNull.Count", expect_ok: false);
AssertEqual("Cannot access member \"Count\" of a null-valued object.",
res.Error["result"]?["description"]?.Value<string>(), "wrong error message");
(_, res) = await EvaluateOnCallFrame(id, "listNull!.Count", expect_ok: false);
AssertEqual("Cannot access member \"Count\" of a null-valued object.",
res.Error["result"]?["description"]?.Value<string>(), "wrong error message");
(_, res) = await EvaluateOnCallFrame(id, "tcNull.memberListNull.Count", expect_ok: false);
AssertEqual("Cannot access member \"memberListNull\" of a null-valued object.",
res.Error["result"]?["description"]?.Value<string>(), "wrong error message");
(_, res) = await EvaluateOnCallFrame(id, "tc.memberListNull.Count", expect_ok: false);
AssertEqual("Cannot access member \"Count\" of a null-valued object.",
res.Error["result"]?["description"]?.Value<string>(), "wrong error message");
(_, res) = await EvaluateOnCallFrame(id, "tcNull?.memberListNull.Count", expect_ok: false);
AssertEqual("Cannot access member \"Count\" of a null-valued object.",
res.Error["result"]?["description"]?.Value<string>(), "wrong error message");
(_, res) = await EvaluateOnCallFrame(id, "listNull?.Count.NonExistingProperty", expect_ok: false);
AssertEqual("Cannot access member \"NonExistingProperty\" of a null-valued object.",
res.Error["result"]?["description"]?.Value<string>(), "wrong error message");
(_, res) = await EvaluateOnCallFrame(id, "listNull?", expect_ok: false);
AssertEqual("Expected expression.",
res.Error["result"]?["description"]?.Value<string>(), "wrong error message");
await CheckEvaluateFail("list.Count.x", "Cannot find member 'x' on a primitive type");
await CheckEvaluateFail("listNull.Count", GetNullReferenceErrorOn("\"Count\""));
await CheckEvaluateFail("listNull!.Count", GetNullReferenceErrorOn("\"Count\""));
await CheckEvaluateFail("tcNull.MemberListNull.Count", GetNullReferenceErrorOn("\"MemberListNull\""));
await CheckEvaluateFail("tc.MemberListNull.Count", GetNullReferenceErrorOn("\"Count\""));
await CheckEvaluateFail("tcNull?.MemberListNull.Count", GetNullReferenceErrorOn("\"Count\""));
await CheckEvaluateFail("listNull?.Count.NonExistingProperty", GetNullReferenceErrorOn("\"NonExistingProperty\""));
await CheckEvaluateFail("tc?.MemberListNull! .Count", GetNullReferenceErrorOn("\"Count\""));
await CheckEvaluateFail("tc?. MemberListNull!.Count", GetNullReferenceErrorOn("\"Count\""));
await CheckEvaluateFail("tc?.MemberListNull.Count", GetNullReferenceErrorOn("\"Count\""));
await CheckEvaluateFail("tc! .MemberListNull!.Count", GetNullReferenceErrorOn("\"Count\""));
await CheckEvaluateFail("tc!.MemberListNull. Count", GetNullReferenceErrorOn("\"Count\""));
await CheckEvaluateFail("tcNull?.Sibling.MemberListNull?.Count", GetNullReferenceErrorOn("\"MemberListNull?\""));
await CheckEvaluateFail("listNull?", "Expected expression.");
await CheckEvaluateFail("listNull!.Count", GetNullReferenceErrorOn("\"Count\""));
await CheckEvaluateFail("x?.p", "Operation '?' not allowed on primitive type - 'x?'");

string GetNullReferenceErrorOn(string name) => $"Expression threw NullReferenceException trying to access {name} on a null-valued object.";

async Task CheckEvaluateFail(string expr, string message)
{
(_, Result _res) = await EvaluateOnCallFrame(id, expr, expect_ok: false);
AssertEqual(message, _res.Error["result"]?["description"]?.Value<string>(), $"Expression '{expr}' - wrong error message");
}
});

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1371,7 +1371,7 @@ public static void Evaluate()
var test = new TestClass();
}
}

public static class PrimitiveTypeMethods
{
public class TestClass
Expand Down Expand Up @@ -1406,8 +1406,9 @@ public static class EvaluateNullableProperties
{
class TestClass
{
public List<int> memberListNull = null;
List<int> memberList = new List<int>() {1, 2};
public List<int> MemberListNull = null;
public List<int> MemberList = new List<int>() {1, 2};
public TestClass Sibling { get; set; }
}
static void Evaluate()
{
Expand All @@ -1417,6 +1418,11 @@ static void Evaluate()
List<int> list = new List<int>() {1};
TestClass tc = new TestClass();
TestClass tcNull = null;
string str = "str#value";
string str_null = null;
int x = 5;
int? x_null = null;
int? x_val = x;
}
}
}
Expand Down