Skip to content

Commit

Permalink
[wasm] [debugger] Eval fixes for static class eval (#61660)
Browse files Browse the repository at this point in the history
* Using current namespace as the default place to serach for the resolved class.

* Add tests for static class, static fields and pausing in async method.

* Added tests for class evaluation.

* Fixing support to the current namespace and adding tests for it

* Assuing that we search within the current assembly first. Removed tests that fail in Consol App.

* Remove a test-duplicate that was not testing static class or static fields.

* Fixing indentation.

* Refixing indentation.

* Refix indentations again.

* Applied the advice about adding new blank lines.

* Changed the current assembly check.

* Extracting the check from the loop. One time check is enough.

* Simplifying multiple test cases into one call.

* Using local function as per review suggestion.

* Added test that was skipped by mistake.

* Added looking for the namespace in all assemblies because there is a chance it will be located out of the current assembly.

* Extracting value based on the current frame, not the top of stack location.

* Test for classes evaluated from different frames.

* Tests for nested static classes.

* Fix for nested static classes.

* Fixed 9 tests from EvaluateOnCallFrame.

* Fixing indentation and spaces.

* Applied review comments for values evaluation.

* Compressed two tests into one with MemberData.

* Added test case of type without namespace (failing).

* Addressed Ankit advices from the review.

* Revert merged nested evaluation changes.

* Incorporate Ankit's changes from d020d36.

* Fix - when both valuesare null we should keep checking (e.g. for nested static classes).

* Added nested tests.

* Redo changes after reverting them in merge.

* Fixed - works with and without namespace.

* Fix merge.

* Using current namespace as the default place to serach for the resolved class.

* Add tests for static class, static fields and pausing in async method.

* Added tests for class evaluation.

* Fixing support to the current namespace and adding tests for it

* Assuing that we search within the current assembly first. Removed tests that fail in Consol App.

* Remove a test-duplicate that was not testing static class or static fields.

* Fixing indentation.

* Extracting the check from the loop. One time check is enough.

* Simplifying multiple test cases into one call.

* Added test that was skipped by mistake.

* Test for classes evaluated from different frames.

* Tests for nested static classes.

* Fix for nested static classes.

* Fixing indentation and spaces.

* Applied review comments for values evaluation.

* Compressed two tests into one with MemberData.

* Addressed Ankit advices from the review.

* Revert merged nested evaluation changes.

* Incorporate Ankit's changes from d020d36.

* Fix - when both valuesare null we should keep checking (e.g. for nested static classes).

* Fix merge.

* Cleanup after rebase.

* Added nested tests.

* Fixed - works with and without namespace.

* Clean-up after rebasing with fix-static-attribute-support.

* Fixed 9 tests from EvaluateOnCallFrame.

* Fixed 18 test types.

* Fix test cases with spaces in the names, e.g. "  this" evaluation.

* Update src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs

Co-authored-by: Larry Ewing <[email protected]>

* Avoid resolving fields of a null value.

* Fix for 4 failing tests of Count evaluation.

* Update src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs

Co-authored-by: Ankit Jain <[email protected]>

* Update src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs

Co-authored-by: Ankit Jain <[email protected]>

* Add null safety.

* Exchanged multiple trims for one.

* Undo nested static changes that will be sumbitted in another PR.

* Revert "Undo nested static changes that will be sumbitted in another PR."

This reverts commit 6a1b7ad.

* Hiding test that has to be fixed in the future.

* Applying Ankit's suggestion about code simplification.

Co-authored-by: DESKTOP-GEPIA6N\Thays <[email protected]>
Co-authored-by: Larry Ewing <[email protected]>
Co-authored-by: Ankit Jain <[email protected]>
  • Loading branch information
4 people authored Nov 24, 2021
1 parent a46358c commit 35815de
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 118 deletions.
13 changes: 5 additions & 8 deletions src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -488,16 +488,13 @@ public TypeInfo(AssemblyInfo assembly, TypeDefinitionHandle typeHandle, TypeDefi
this.type = type;
methods = new List<MethodInfo>();
Name = metadataReader.GetString(type.Name);
if (type.IsNested)
var declaringType = type;
while (declaringType.IsNested)
{
var declaringType = metadataReader.GetTypeDefinition(type.GetDeclaringType());
Name = metadataReader.GetString(declaringType.Name) + "/" + Name;
Namespace = metadataReader.GetString(declaringType.Namespace);
}
else
{
Namespace = metadataReader.GetString(type.Namespace);
declaringType = metadataReader.GetTypeDefinition(declaringType.GetDeclaringType());
Name = metadataReader.GetString(declaringType.Name) + "." + Name;
}
Namespace = metadataReader.GetString(declaringType.Namespace);
if (Namespace.Length > 0)
FullName = Namespace + "." + Name;
else
Expand Down
249 changes: 153 additions & 96 deletions src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,74 +77,99 @@ public async Task<JObject> GetValueFromObject(JToken objRet, CancellationToken t
return null;
}

public async Task<JObject> TryToRunOnLoadedClasses(string varName, CancellationToken token)
public async Task<(JObject containerObject, string remaining)> ResolveStaticMembersInStaticTypes(string varName, CancellationToken token)
{
string classNameToFind = "";
string[] parts = varName.Split(".");
var typeId = -1;
foreach (string part in parts)
string[] parts = varName.Split(".", StringSplitOptions.TrimEntries);
var store = await proxy.LoadStore(sessionId, token);
var methodInfo = context.CallStack.FirstOrDefault(s => s.Id == scopeId)?.Method?.Info;

if (methodInfo == null)
return (null, null);

int typeId = -1;
for (int i = 0; i < parts.Length; i++)
{
if (classNameToFind.Length > 0)
classNameToFind += ".";
classNameToFind += part.Trim();
string part = parts[i];

if (typeId != -1)
{
var fields = await context.SdbAgent.GetTypeFields(typeId, token);
foreach (var field in fields)
{
if (field.Name == part.Trim())
{
var isInitialized = await context.SdbAgent.TypeIsInitialized(typeId, token);
if (isInitialized == 0)
{
isInitialized = await context.SdbAgent.TypeInitialize(typeId, token);
}
var valueRet = await context.SdbAgent.GetFieldValue(typeId, field.Id, token);
return await GetValueFromObject(valueRet, token);
}
}
var methodId = await context.SdbAgent.GetPropertyMethodIdByName(typeId, part.Trim(), token);
if (methodId != -1)
JObject memberObject = await FindStaticMemberInType(part, typeId);
if (memberObject != null)
{
using var commandParamsObjWriter = new MonoBinaryWriter();
commandParamsObjWriter.Write(0); //param count
var retMethod = await context.SdbAgent.InvokeMethod(commandParamsObjWriter.GetParameterBuffer(), methodId, "methodRet", token);
return await GetValueFromObject(retMethod, token);
string remaining = null;
if (i < parts.Length - 1)
remaining = string.Join('.', parts[(i + 1)..]);

return (memberObject, remaining);
}

// Didn't find a member named `part` in `typeId`.
// Could be a nested type. Let's continue the search
// with `part` added to the type name

typeId = -1;
}
var store = await proxy.LoadStore(sessionId, token);
var methodInfo = context.CallStack.FirstOrDefault(s => s.Id == scopeId)?.Method?.Info;
var classNameToFindWithNamespace =
string.IsNullOrEmpty(methodInfo?.TypeInfo?.Namespace) ?
classNameToFind :
methodInfo.TypeInfo.Namespace + "." + classNameToFind;

var searchResult = await TryFindNameInAssembly(store.assemblies, classNameToFindWithNamespace);
if (searchResult == null)
searchResult = await TryFindNameInAssembly(store.assemblies, classNameToFind);
if (searchResult != null)
typeId = (int)searchResult;

async Task<int?> TryGetTypeIdFromName(string typeName, AssemblyInfo assembly)

if (classNameToFind.Length > 0)
classNameToFind += ".";
classNameToFind += part;

if (!string.IsNullOrEmpty(methodInfo?.TypeInfo?.Namespace))
{
var type = assembly.GetTypeByName(typeName);
if (type == null)
return null;
return await context.SdbAgent.GetTypeIdFromToken(assembly.DebugId, type.Token, token);
typeId = await FindStaticTypeId(methodInfo?.TypeInfo?.Namespace + "." + classNameToFind);
if (typeId != -1)
continue;
}
typeId = await FindStaticTypeId(classNameToFind);
}

return (null, null);

async Task<int?> TryFindNameInAssembly(List<AssemblyInfo> assemblies, string name)
async Task<JObject> FindStaticMemberInType(string name, int typeId)
{
var fields = await context.SdbAgent.GetTypeFields(typeId, token);
foreach (var field in fields)
{
foreach (var asm in assemblies)
if (field.Name != name)
continue;

var isInitialized = await context.SdbAgent.TypeIsInitialized(typeId, token);
if (isInitialized == 0)
{
var typeId = await TryGetTypeIdFromName(name, asm);
if (typeId != null)
return typeId;
isInitialized = await context.SdbAgent.TypeInitialize(typeId, token);
}
return null;
var valueRet = await context.SdbAgent.GetFieldValue(typeId, field.Id, token);

return await GetValueFromObject(valueRet, token);
}

var methodId = await context.SdbAgent.GetPropertyMethodIdByName(typeId, name, token);
if (methodId != -1)
{
using var commandParamsObjWriter = new MonoBinaryWriter();
commandParamsObjWriter.Write(0); //param count
var retMethod = await context.SdbAgent.InvokeMethod(commandParamsObjWriter.GetParameterBuffer(), methodId, "methodRet", token);
return await GetValueFromObject(retMethod, token);
}
return null;
}

async Task<int> FindStaticTypeId(string typeName)
{
foreach (var asm in store.assemblies)
{
var type = asm.GetTypeByName(typeName);
if (type == null)
continue;

int id = await context.SdbAgent.GetTypeIdFromToken(asm.DebugId, type.Token, token);
if (id != -1)
return id;
}

return -1;
}
return null;
}

// Checks Locals, followed by `this`
Expand All @@ -154,73 +179,105 @@ public async Task<JObject> Resolve(string varName, CancellationToken token)
if (varName.Contains('('))
return null;

string[] parts = varName.Split(".");
JObject rootObject = null;

if (scopeCache.MemberReferences.TryGetValue(varName, out JObject ret)) {
if (scopeCache.MemberReferences.TryGetValue(varName, out JObject ret))
return ret;
}

if (scopeCache.ObjectFields.TryGetValue(varName, out JObject valueRet)) {
if (scopeCache.ObjectFields.TryGetValue(varName, out JObject valueRet))
return await GetValueFromObject(valueRet, token);
}

foreach (string part in parts)
string[] parts = varName.Split(".");
if (parts.Length == 0)
return null;

JObject retObject = await ResolveAsLocalOrThisMember(parts[0]);
if (retObject != null && parts.Length > 1)
retObject = await ResolveAsInstanceMember(string.Join('.', parts[1..]), retObject);

if (retObject == null)
{
string partTrimmed = part.Trim();
if (partTrimmed == "")
return null;
if (rootObject != null)
(retObject, string remaining) = await ResolveStaticMembersInStaticTypes(varName, token);
if (!string.IsNullOrEmpty(remaining))
{
if (rootObject?["subtype"]?.Value<string>() == "null")
return null;
if (DotnetObjectId.TryParse(rootObject?["objectId"]?.Value<string>(), out DotnetObjectId objectId))
if (retObject?["subtype"]?.Value<string>() == "null")
{
var rootResObj = await proxy.RuntimeGetPropertiesInternal(sessionId, objectId, null, token);
var objRet = rootResObj.FirstOrDefault(objPropAttr => objPropAttr["name"].Value<string>() == partTrimmed);
if (objRet == null)
return null;

rootObject = await GetValueFromObject(objRet, token);
// NRE on null.$remaining
retObject = null;
}
else
{
retObject = await ResolveAsInstanceMember(remaining, retObject);
}
continue;
}
}

scopeCache.MemberReferences[varName] = retObject;
return retObject;

async Task<JObject> ResolveAsLocalOrThisMember(string name)
{
var nameTrimmed = name.Trim();
if (scopeCache.Locals.Count == 0 && !localsFetched)
{
Result scope_res = await proxy.GetScopeProperties(sessionId, scopeId, token);
if (scope_res.IsErr)
throw new Exception($"BUG: Unable to get properties for scope: {scopeId}. {scope_res}");
localsFetched = true;
}
if (scopeCache.Locals.TryGetValue(partTrimmed, out JObject obj))
{
rootObject = obj["value"]?.Value<JObject>();
}
else if (scopeCache.Locals.TryGetValue("this", out JObject objThis))

if (scopeCache.Locals.TryGetValue(nameTrimmed, out JObject obj))
return obj["value"]?.Value<JObject>();

if (!scopeCache.Locals.TryGetValue("this", out JObject objThis))
return null;

if (!DotnetObjectId.TryParse(objThis?["value"]?["objectId"]?.Value<string>(), out DotnetObjectId objectId))
return null;

var rootResObj = await proxy.RuntimeGetPropertiesInternal(sessionId, objectId, null, token);
var objRet = rootResObj.FirstOrDefault(objPropAttr => objPropAttr["name"].Value<string>() == nameTrimmed);
if (objRet != null)
return await GetValueFromObject(objRet, token);

return null;
}

async Task<JObject> ResolveAsInstanceMember(string expr, JObject baseObject)
{
JObject resolvedObject = baseObject;
string[] parts = expr.Split('.');
for (int i = 0; i < parts.Length; i++)
{
if (partTrimmed == "this")
{
rootObject = objThis?["value"].Value<JObject>();
}
else if (DotnetObjectId.TryParse(objThis?["value"]?["objectId"]?.Value<string>(), out DotnetObjectId objectId))
string partTrimmed = parts[i].Trim();
if (partTrimmed.Length == 0)
return null;

if (!DotnetObjectId.TryParse(resolvedObject?["objectId"]?.Value<string>(), out DotnetObjectId objectId))
return null;

var resolvedResObj = await proxy.RuntimeGetPropertiesInternal(sessionId, objectId, null, token);
var objRet = resolvedResObj.FirstOrDefault(objPropAttr => objPropAttr["name"]?.Value<string>() == partTrimmed);
if (objRet == null)
return null;

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

if (resolvedObject["subtype"]?.Value<string>() == "null")
{
var rootResObj = await proxy.RuntimeGetPropertiesInternal(sessionId, objectId, null, token);
var objRet = rootResObj.FirstOrDefault(objPropAttr => objPropAttr["name"].Value<string>() == partTrimmed);
if (objRet != null)
if (i < parts.Length - 1)
{
rootObject = await GetValueFromObject(objRet, token);
}
else
{
break;
// there is some parts remaining, and can't
// do null.$remaining
return null;
}

return resolvedObject;
}
}

return resolvedObject;
}
if (rootObject == null)
rootObject = await TryToRunOnLoadedClasses(varName, token);
scopeCache.MemberReferences[varName] = rootObject;
return rootObject;
}

public async Task<JObject> Resolve(ElementAccessExpressionSyntax elementAccess, Dictionary<string, JObject> memberAccessValues, JObject indexObject, CancellationToken token)
Expand Down
4 changes: 2 additions & 2 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,8 @@ protected override async Task<bool> AcceptCommand(MessageId id, string method, J
{
// Maybe this is an async method, in which case the debug info is attached
// to the async method implementation, in class named:
// `{type_name}/<method_name>::MoveNext`
methodInfo = assembly.TypesByName.Values.SingleOrDefault(t => t.FullName.StartsWith($"{typeName}/<{methodName}>"))?
// `{type_name}.<method_name>::MoveNext`
methodInfo = assembly.TypesByName.Values.SingleOrDefault(t => t.FullName.StartsWith($"{typeName}.<{methodName}>"))?
.Methods.FirstOrDefault(mi => mi.Name == "MoveNext");
}

Expand Down
Loading

0 comments on commit 35815de

Please sign in to comment.