Skip to content
Draft
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
1 change: 1 addition & 0 deletions ChaosUtil.Debug.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<Import Project="submodules/ChaosPresets.Targets/Configurations.targets" />

<!-- References -->
<Import Project="submodules/ChaosPresets.References/ProjectReferences/ChaosUtil.Primitives.targets" />
<Import Project="submodules/ChaosPresets.References/ProjectReferences/ChaosUtil.Reflection.targets" />
<ItemGroup>
<Reference Include="System" />
Expand Down
40 changes: 40 additions & 0 deletions source/AdvancedDebuggerDisplay/EvaluatedToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Linq;

namespace ChaosUtil.Debug.AdvancedDebuggerDisplay
{
class EvaluatedToken : Token
{
static string Trim(string str) => str.Trim();

readonly Type targetType;
readonly object obj;

public EvaluatedToken(object obj, string token)
: base(token)
{
Type targetType = obj.GetType();
foreach (string member in token.Split('.').Select(Trim))
{
// TODO: support indexers

if (Members.Field.GetValue(ref targetType, ref obj, member))
continue;

if (Members.Property.GetValue(ref targetType, ref obj, member))
continue;

if (Members.Method.GetValue(ref targetType, ref obj, member))
continue;

throw new Exception($"Could not evaluate \"{member}\".");
}

this.targetType = targetType;
this.obj = obj;
}

public override string Value()
=> AdvancedDebuggerDisplayExtensions.DebugDisplayInternal(targetType, obj);
}
}
31 changes: 31 additions & 0 deletions source/AdvancedDebuggerDisplay/Members/Field.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Reflection;
using FieldMap = System.Collections.Generic.Dictionary<
System.Type,
System.Collections.Generic.Dictionary<
string,
System.Reflection.FieldInfo
>
>;

namespace ChaosUtil.Debug.AdvancedDebuggerDisplay.Members
{
static class Field
{
static readonly FieldMap fieldMap = new FieldMap();

public static bool GetValue(ref Type targetType, ref object obj, string fieldName)
{
Type t = targetType;
FieldInfo field = MemberMap.GetMember(fieldMap, targetType, fieldName, flags => t.GetField(fieldName, flags));
if (field != null)
{
obj = field.GetValue(obj);
targetType = field.FieldType;
return true;
}

return false;
}
}
}
28 changes: 28 additions & 0 deletions source/AdvancedDebuggerDisplay/Members/MemberMap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Reflection;
using SysCol = System.Collections.Generic;

namespace ChaosUtil.Debug.AdvancedDebuggerDisplay.Members
{
static class MemberMap
{
public static Member GetMember<Member>(
SysCol.Dictionary<Type, SysCol.Dictionary<string, Member>> memberMap,
Type type,
string memberName,
Func<BindingFlags, Member> getMember
)
where Member : class
{
SysCol.Dictionary<string, Member> typeMemberMap;
if (!memberMap.TryGetValue(type, out typeMemberMap))
memberMap[type] = typeMemberMap = new SysCol.Dictionary<string, Member>();

Member member;
if (!typeMemberMap.TryGetValue(memberName, out member))
typeMemberMap[memberName] = member = getMember(BindingFlags.Public | BindingFlags.Instance) ?? getMember(BindingFlags.NonPublic | BindingFlags.Instance);

return member;
}
}
}
98 changes: 98 additions & 0 deletions source/AdvancedDebuggerDisplay/Members/Method.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using ChaosUtil.Primitives;
using System;
using System.Reflection;
using MethodMap = System.Collections.Generic.Dictionary<
System.Type,
System.Collections.Generic.Dictionary<
string,
System.Reflection.MethodInfo
>
>;

namespace ChaosUtil.Debug.AdvancedDebuggerDisplay.Members
{
static class Method
{
static readonly MethodMap methodMap = new MethodMap();

static readonly MethodInfo debugDisplayMethod = typeof(AdvancedDebuggerDisplayExtensions).GetMethod(
nameof(AdvancedDebuggerDisplayExtensions.DebugDisplay),
BindingFlags.Public | BindingFlags.Static
);

public static bool GetValue(ref Type targetType, ref object obj, string member)
=> GetMethodObject(ref targetType, ref obj, member)
|| GetMethodInvocation(ref targetType, ref obj, member);

static MethodInfo FindMethod(Type targetType, string methodName, BindingFlags flags)
{
MethodInfo meth = targetType.GetMethod(methodName, flags, null, Array<Type>.empty, Array<ParameterModifier>.empty);
if (meth != null)
return meth;

// TODO: properly support extension methods
if (methodName == nameof(AdvancedDebuggerDisplayExtensions.DebugDisplay))
return debugDisplayMethod.MakeGenericMethod(targetType);

return null;
}

public static bool GetMethodObject(ref Type targetType, ref object obj, string methodName)
{
Type t = targetType;
MethodInfo meth = MemberMap.GetMember(
methodMap,
targetType,
methodName,
flags => FindMethod(t, methodName, flags)
);
if (meth != null)
{
targetType = typeof(MethodInfo);
obj = meth;
return true;
}
else
return false;
}

public static bool GetMethodInvocation(ref Type targetType, ref object obj, string invocation)
{
if (invocation.Length < 3)
return false; // at least one letter for method name plus one pair of parantheses

int paraOpen = invocation.IndexOf('(');
if (paraOpen < 0)
return false;

int paraClose = invocation.LastIndexOf(')');
if (paraClose != invocation.Length - 1)
return false;

int argsLength = paraClose - paraOpen - 1;
if (argsLength != 0)
// TODO: support method arguments
throw new NotImplementedException("Currently only parameterless method calls are supported.");

string methodName = invocation.Substring(0, paraOpen);
Type t = targetType;
MethodInfo meth = MemberMap.GetMember(
methodMap,
targetType,
methodName,
flags => FindMethod(t, methodName, flags)
);
if (meth != null)
{
if (meth.IsStatic)
obj = meth.Invoke(null, new[] { obj });
else
obj = meth.Invoke(obj, Array<object>.empty);
targetType = meth.ReturnType;
return true;
}
else
return false;
}
}
}
31 changes: 31 additions & 0 deletions source/AdvancedDebuggerDisplay/Members/Property.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Reflection;
using PropertyMap = System.Collections.Generic.Dictionary<
System.Type,
System.Collections.Generic.Dictionary<
string,
System.Reflection.PropertyInfo
>
>;

namespace ChaosUtil.Debug.AdvancedDebuggerDisplay.Members
{
static class Property
{
static readonly PropertyMap propertyMap = new PropertyMap();

public static bool GetValue(ref Type targetType, ref object obj, string member)
{
Type t = targetType;
PropertyInfo property = MemberMap.GetMember(propertyMap, targetType, member, flags => t.GetProperty(member, flags));
if (property != null)
{
obj = property.GetValue(obj);
targetType = property.PropertyType;
return true;
}

return false;
}
}
}
12 changes: 12 additions & 0 deletions source/AdvancedDebuggerDisplay/TerminalToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace ChaosUtil.Debug.AdvancedDebuggerDisplay
{
class TerminalToken : Token
{
public TerminalToken(string token)
: base(token)
{ }

public override string Value()
=> token;
}
}
16 changes: 16 additions & 0 deletions source/AdvancedDebuggerDisplay/Token.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace ChaosUtil.Debug.AdvancedDebuggerDisplay
{
abstract class Token
{
public static string Evaluate(Token token) => token.Value();

public readonly string token;

public Token(string token)
{
this.token = token;
}

public abstract string Value();
}
}
84 changes: 84 additions & 0 deletions source/AdvancedDebuggerDisplayExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using ChaosUtil.Debug.AdvancedDebuggerDisplay;
using ChaosUtil.Reflection;
using System;
using System.Diagnostics;
using System.Linq;
using SysCol = System.Collections.Generic;

/// <summary> Provides a way of evaluating debugger displays at runtime and constructing recursive debugger displays. </summary>
public static class AdvancedDebuggerDisplayExtensions
{
// TODO: Provide methods to generate debugger displays that automatically list public (or public and non-public) members

static readonly SysCol.Dictionary<Type, DebuggerDisplayAttribute> displayAttributes
= new SysCol.Dictionary<Type, DebuggerDisplayAttribute>();

/// <summary>
/// Returns a string representation of <paramref name="obj"/> as displayed by the debugger.
/// If a <see cref="DebuggerDisplayAttribute"/> is attached to <typeparamref name="T"/>,
/// it will be used to generate the debugger display.
/// Otherwise <see cref="object.ToString"/> will be used as fallback.
/// </summary>
/// <typeparam name="T"> The type of the object to be displayed. </typeparam>
/// <param name="obj"> The object to be displayed. </param>
public static string DebugDisplay<T>(this T obj)
=> DebugDisplayInternal(typeof(T), obj);

internal static string DebugDisplayInternal(Type t, object obj)
{
DebuggerDisplayAttribute debuggerDisplay;
if (!displayAttributes.TryGetValue(t, out debuggerDisplay))
displayAttributes[t] = debuggerDisplay = t.GetAttributes<DebuggerDisplayAttribute>(true).FirstOrDefault();

if (debuggerDisplay == null)
return obj?.ToString() ?? "null";

return string.Concat(Tokenize(obj, debuggerDisplay.Value).Select(Token.Evaluate));
}

static SysCol.IEnumerable<Token> Tokenize(object context, string debuggerDisplay)
{
bool isComplicated = false;
int start = 0;

while (start < debuggerDisplay.Length)
if (isComplicated)
{
int depth = 0;
for (int end = start; end < debuggerDisplay.Length; end++)
switch (debuggerDisplay[end])
{
case '{':
depth++;
break;

case '}':
if (--depth < 0)
{
string token = debuggerDisplay.Substring(start, end - start);
yield return new EvaluatedToken(context, token);
start = end + 1;
isComplicated = false;
goto token_found;
}
break;
}

// TODO: allow escaping braces with double braces
throw new Exception("Illegal braces!");

token_found:;
}
else
{
int end = debuggerDisplay.IndexOf('{', start);
if (end < 0)
end = debuggerDisplay.Length;

string terminal = debuggerDisplay.Substring(start, end - start);
yield return new TerminalToken(terminal);
start = end + 1;
isComplicated = true;
}
}
}