Skip to content

Commit 84513a2

Browse files
authored
Display parents' members on GetProperties. (#68907)
* Prepared tests with the same members as in Console App. * Adding parents' members to match the expected result in tests. * Increasing the timeout because some EvaluateOnCallFrameTests tests take up to 63 sec. * Reverting timeout editions. * Applied refactoring suggestions from @radical and @thaystg. * Added new text cases with addnotation which fields are displayed incorrectly and should be fixed. * More detailed error message for count mismatch. * Reorganising tests with order: grouping similar members. * Fixing override/new problem. * Fixing test to reflect ConsoleApp behavior. * Wrong name - it should be tested with the new class: 2. * Applied @radical's suggestion about backingField. * @radical's changes - refactor + one new test case. * Checking Getter vals; AssertEqual(cnt) not needed. * New test cases, some to be fixed in the follow-up issue. * Added @radical's suggestions. * Different DataTimes for each property.
1 parent b290391 commit 84513a2

File tree

6 files changed

+426
-168
lines changed

6 files changed

+426
-168
lines changed

src/mono/wasm/debugger/BrowserDebugProxy/MemberObjectsExplorer.cs

Lines changed: 141 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -291,10 +291,10 @@ JArray GetHiddenElement()
291291
}
292292
}
293293

294-
public static async Task<Dictionary<string, JObject>> GetNonAutomaticPropertyValues(
294+
public static async Task<Dictionary<string, JObject>> ExpandPropertyValues(
295295
MonoSDBHelper sdbHelper,
296296
int typeId,
297-
string containerTypeName,
297+
string typeName,
298298
ArraySegment<byte> getterParamsBuffer,
299299
bool isAutoExpandable,
300300
DotnetObjectId objectId,
@@ -311,6 +311,7 @@ public static async Task<Dictionary<string, JObject>> GetNonAutomaticPropertyVal
311311
var nProperties = retDebuggerCmdReader.ReadInt32();
312312
var typeInfo = await sdbHelper.GetTypeInfo(typeId, token);
313313
var typePropertiesBrowsableInfo = typeInfo?.Info?.DebuggerBrowsableProperties;
314+
var parentSuffix = typeName.Split('.')[^1];
314315

315316
GetMembersResult ret = new();
316317
for (int i = 0; i < nProperties; i++)
@@ -326,82 +327,139 @@ public static async Task<Dictionary<string, JObject>> GetNonAutomaticPropertyVal
326327
continue;
327328

328329
MethodInfoWithDebugInformation getterInfo = await sdbHelper.GetMethodInfo(getMethodId, token);
329-
MethodAttributes getterAttrs = getterInfo?.Info.Attributes ?? MethodAttributes.Public;
330-
getterAttrs &= MethodAttributes.MemberAccessMask;
330+
MethodAttributes getterAttrs = getterInfo.Info.Attributes;
331+
MethodAttributes getterMemberAccessAttrs = getterAttrs & MethodAttributes.MemberAccessMask;
331332

332333
typePropertiesBrowsableInfo.TryGetValue(propName, out DebuggerBrowsableState? state);
333334

334-
if (allMembers.TryGetValue(propName, out JObject backingField))
335+
// handle parents' members:
336+
if (!allMembers.TryGetValue(propName, out JObject existingMember))
335337
{
336-
if (backingField["__isBackingField"]?.Value<bool>() == true)
337-
{
338-
// Update backingField's access with the one from the property getter
339-
backingField["__section"] = getterAttrs switch
340-
{
341-
MethodAttributes.Private => "private",
342-
MethodAttributes.Public => "result",
343-
_ => "internal"
344-
};
345-
backingField["__state"] = state?.ToString();
346-
347-
if (state is not null)
348-
{
349-
string namePrefix = GetNamePrefixForValues(propName, containerTypeName, isOwn, state);
350-
351-
string backingFieldTypeName = backingField["value"]?["className"]?.Value<string>();
352-
var expanded = await GetExpandedMemberValues(
353-
sdbHelper, backingFieldTypeName, namePrefix, backingField, state, includeStatic, token);
354-
backingField.Remove();
355-
allMembers.Remove(propName);
356-
foreach (JObject evalue in expanded)
357-
allMembers[evalue["name"].Value<string>()] = evalue;
358-
}
359-
}
338+
// new member
339+
await AddProperty(getMethodId, state, propName, getterMemberAccessAttrs);
340+
continue;
341+
}
342+
343+
bool isExistingMemberABackingField = existingMember["__isBackingField"]?.Value<bool>() == true;
344+
if (isOwn && !isExistingMemberABackingField)
345+
{
346+
// repeated propname on the same type! cannot happen
347+
throw new Exception($"Internal Error: should not happen. propName: {propName}. Existing all members: {string.Join(",", allMembers.Keys)}");
348+
}
360349

361-
// derived type already had a member of this name
350+
bool isExistingMemberABackingFieldOwnedByThisType = isExistingMemberABackingField && existingMember["__owner"]?.Value<string>() == typeName;
351+
if (isExistingMemberABackingField && (isOwn || isExistingMemberABackingFieldOwnedByThisType))
352+
{
353+
// this is the property corresponding to the backing field in *this* type
354+
// `isOwn` would mean that this is the first type that we are looking at
355+
await UpdateBackingFieldWithPropertyAttributes(existingMember, propName, getterMemberAccessAttrs, state);
362356
continue;
363357
}
364-
else
358+
359+
var overriddenOrHiddenPropName = $"{propName} ({parentSuffix})";
360+
MethodAttributes vtableLayout = getterAttrs & MethodAttributes.VtableLayoutMask;
361+
bool wasOverriddenByDerivedType = (vtableLayout & MethodAttributes.NewSlot) == MethodAttributes.NewSlot;
362+
if (wasOverriddenByDerivedType)
363+
{
364+
/*
365+
* property was overridden by a derived type member. We want to show
366+
* only the overridden members. So, remove the backing field
367+
* for this auto-property that was added, with the type name suffix
368+
*
369+
* Two cases:
370+
* 1. auto-prop in base, overridden by auto-prop in derived
371+
* 2. auto-prop in base, overridden by prop in derived
372+
*
373+
* And in both cases we want to remove the backing field for the auto-prop for
374+
* *this* base type
375+
*/
376+
allMembers.Remove(overriddenOrHiddenPropName);
377+
continue;
378+
}
379+
380+
/*
381+
* property was *hidden* by a derived type member. In this case, we
382+
* want to show *both* the members
383+
*/
384+
385+
JObject backingFieldForHiddenProp = allMembers.GetValueOrDefault(overriddenOrHiddenPropName);
386+
if (backingFieldForHiddenProp is null || backingFieldForHiddenProp["__isBackingField"]?.Value<bool>() != true)
365387
{
366-
string returnTypeName = await sdbHelper.GetReturnType(getMethodId, token);
367-
JObject propRet = null;
368-
if (isAutoExpandable || (state is DebuggerBrowsableState.RootHidden && IsACollectionType(returnTypeName)))
388+
// hiding with a non-auto property, so nothing to adjust
389+
// add the new property
390+
await AddProperty(getMethodId, state, overriddenOrHiddenPropName, getterMemberAccessAttrs);
391+
continue;
392+
}
393+
394+
await UpdateBackingFieldWithPropertyAttributes(backingFieldForHiddenProp, overriddenOrHiddenPropName, getterMemberAccessAttrs, state);
395+
}
396+
397+
return allMembers;
398+
399+
async Task UpdateBackingFieldWithPropertyAttributes(JObject backingField, string autoPropName, MethodAttributes getterMemberAccessAttrs, DebuggerBrowsableState? state)
400+
{
401+
backingField["__section"] = getterMemberAccessAttrs switch
402+
{
403+
MethodAttributes.Private => "private",
404+
MethodAttributes.Public => "result",
405+
_ => "internal"
406+
};
407+
backingField["__state"] = state?.ToString();
408+
409+
if (state is null)
410+
return;
411+
412+
string namePrefix = GetNamePrefixForValues(autoPropName, typeName, isOwn, state);
413+
string backingPropTypeName = backingField["value"]?["className"]?.Value<string>();
414+
var expanded = await GetExpandedMemberValues(
415+
sdbHelper, backingPropTypeName, namePrefix, backingField, state, includeStatic, token);
416+
backingField.Remove();
417+
allMembers.Remove(autoPropName);
418+
foreach (JObject evalue in expanded)
419+
allMembers[evalue["name"].Value<string>()] = evalue;
420+
}
421+
422+
async Task AddProperty(int getMethodId, DebuggerBrowsableState? state, string propNameWithSufix, MethodAttributes getterAttrs)
423+
{
424+
string returnTypeName = await sdbHelper.GetReturnType(getMethodId, token);
425+
JObject propRet = null;
426+
if (isAutoExpandable || (state is DebuggerBrowsableState.RootHidden && IsACollectionType(returnTypeName)))
427+
{
428+
try
369429
{
370-
try
371-
{
372-
propRet = await sdbHelper.InvokeMethod(getterParamsBuffer, getMethodId, token, name: propName);
373-
}
374-
catch (Exception)
375-
{
376-
continue;
377-
}
430+
propRet = await sdbHelper.InvokeMethod(getterParamsBuffer, getMethodId, token, name: propNameWithSufix);
378431
}
379-
else
380-
propRet = GetNotAutoExpandableObject(getMethodId, propName);
381-
382-
propRet["isOwn"] = isOwn;
383-
propRet["__section"] = getterAttrs switch
432+
catch (Exception)
384433
{
385-
MethodAttributes.Private => "private",
386-
MethodAttributes.Public => "result",
387-
_ => "internal"
388-
};
389-
propRet["__state"] = state?.ToString();
390-
391-
string namePrefix = GetNamePrefixForValues(propName, containerTypeName, isOwn, state);
392-
var expandedMembers = await GetExpandedMemberValues(
393-
sdbHelper, returnTypeName, namePrefix, propRet, state, includeStatic, token);
394-
foreach (var member in expandedMembers)
434+
propRet = GetNotAutoExpandableObject(getMethodId, propNameWithSufix);
435+
}
436+
}
437+
else
438+
{
439+
propRet = GetNotAutoExpandableObject(getMethodId, propNameWithSufix);
440+
}
441+
442+
propRet["isOwn"] = isOwn;
443+
propRet["__section"] = getterAttrs switch
444+
{
445+
MethodAttributes.Private => "private",
446+
MethodAttributes.Public => "result",
447+
_ => "internal"
448+
};
449+
propRet["__state"] = state?.ToString();
450+
451+
string namePrefix = GetNamePrefixForValues(propNameWithSufix, typeName, isOwn, state);
452+
var expandedMembers = await GetExpandedMemberValues(
453+
sdbHelper, returnTypeName, namePrefix, propRet, state, includeStatic, token);
454+
foreach (var member in expandedMembers)
455+
{
456+
var key = member["name"]?.Value<string>();
457+
if (key != null)
395458
{
396-
var key = member["name"]?.Value<string>();
397-
if (key != null)
398-
{
399-
allMembers.TryAdd(key, member as JObject);
400-
}
459+
allMembers.TryAdd(key, member as JObject);
401460
}
402461
}
403462
}
404-
return allMembers;
405463

406464
JObject GetNotAutoExpandableObject(int methodId, string propertyName)
407465
{
@@ -488,14 +546,14 @@ public static async Task<GetMembersResult> GetObjectMemberValues(
488546
foreach (var f in allFields)
489547
f["__hidden"] = true;
490548
}
491-
AddOnlyNewValuesByNameTo(allFields, allMembers, isOwn);
549+
AddOnlyNewFieldValuesByNameTo(allFields, allMembers, typeName, isOwn);
492550
}
493551

494552
// skip loading properties if not necessary
495553
if (!getCommandType.HasFlag(GetObjectCommandOptions.WithProperties))
496554
return GetMembersResult.FromValues(allMembers.Values, sortByAccessLevel);
497555

498-
allMembers = await GetNonAutomaticPropertyValues(
556+
allMembers = await ExpandPropertyValues(
499557
sdbHelper,
500558
typeId,
501559
typeName,
@@ -518,15 +576,29 @@ public static async Task<GetMembersResult> GetObjectMemberValues(
518576
}
519577
return GetMembersResult.FromValues(allMembers.Values, sortByAccessLevel);
520578

521-
static void AddOnlyNewValuesByNameTo(JArray namedValues, IDictionary<string, JObject> valuesDict, bool isOwn)
579+
static void AddOnlyNewFieldValuesByNameTo(JArray namedValues, IDictionary<string, JObject> valuesDict, string typeName, bool isOwn)
522580
{
523581
foreach (var item in namedValues)
524582
{
525-
var key = item["name"]?.Value<string>();
526-
if (key != null)
583+
var name = item["name"]?.Value<string>();
584+
if (name == null)
585+
continue;
586+
587+
if (valuesDict.TryAdd(name, item as JObject))
527588
{
528-
valuesDict.TryAdd(key, item as JObject);
589+
// new member
590+
if (item["__isBackingField"]?.Value<bool>() == true)
591+
item["__owner"] = typeName;
592+
continue;
529593
}
594+
595+
if (isOwn)
596+
throw new Exception($"Internal Error: found an existing member on own type. item: {item}, typeName: {typeName}");
597+
598+
var parentSuffix = typeName.Split('.')[^1];
599+
var parentMemberName = $"{name} ({parentSuffix})";
600+
valuesDict.Add(parentMemberName, item as JObject);
601+
item["name"] = parentMemberName;
530602
}
531603
}
532604
}

src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ public async Task ExpandPropertyValues(MonoSDBHelper sdbHelper, bool splitMember
270270
// isParent:
271271
if (i != 0) typeId = getParentsReader.ReadInt32();
272272

273-
allMembers = await MemberObjectsExplorer.GetNonAutomaticPropertyValues(
273+
allMembers = await MemberObjectsExplorer.ExpandPropertyValues(
274274
sdbHelper,
275275
typeId,
276276
className,

src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,20 @@ internal async Task CheckProps(JToken actual, object exp_o, string label, int nu
732732
if (!skip_num_fields_check)
733733
{
734734
num_fields = num_fields < 0 ? exp.Values<JToken>().Count() : num_fields;
735-
Assert.True(num_fields == actual.Count(), $"[{label}] Number of fields don't match, Expected: {num_fields}, Actual: {actual.Count()}");
735+
var expected_str = string.Join(", ",
736+
exp.Children()
737+
.Select(e => e is JProperty jprop ? jprop.Name : e["name"]?.Value<string>())
738+
.Where(e => !string.IsNullOrEmpty(e))
739+
.OrderBy(e => e));
740+
741+
var actual_str = string.Join(", ",
742+
actual.Children()
743+
.Select(e => e["name"]?.Value<string>())
744+
.Where(e => !string.IsNullOrEmpty(e))
745+
.OrderBy(e => e));
746+
Assert.True(num_fields == actual.Count(), $"[{label}] Number of fields don't match, Expected: {num_fields}, Actual: {actual.Count()}.{Environment.NewLine}"
747+
+ $" Expected: {expected_str}{Environment.NewLine}"
748+
+ $" Actual: {actual_str}");
736749
}
737750

738751
foreach (var kvp in exp)
@@ -756,8 +769,6 @@ internal async Task CheckProps(JToken actual, object exp_o, string label, int nu
756769
else if (exp_val["__custom_type"] != null && exp_val["__custom_type"]?.Value<string>() == "getter")
757770
{
758771
// hack: for getters, actual won't have a .value
759-
// are we doing it on purpose? Why? CHECK if properties are displayed in Browser/VS, if not revert the value field here
760-
// we should be leaving properties, not their backing fields
761772
await CheckCustomType(actual_obj, exp_val, $"{label}#{exp_name}");
762773
}
763774
else

0 commit comments

Comments
 (0)