From 78abcaee8e697a01ed6a0b81a8d357fc8d5feb80 Mon Sep 17 00:00:00 2001 From: InsanityCode <56584762+InsanityCode@users.noreply.github.com> Date: Thu, 28 Mar 2024 16:49:00 +0100 Subject: [PATCH 1/2] add AdvancedDebuggerDisplayExtensions --- ChaosUtil.Debug.csproj | 1 + .../AdvancedDebuggerDisplay/EvaluatedToken.cs | 40 ++++++++ .../AdvancedDebuggerDisplay/Members/Field.cs | 31 ++++++ .../Members/MemberMap.cs | 28 ++++++ .../AdvancedDebuggerDisplay/Members/Method.cs | 98 +++++++++++++++++++ .../Members/Property.cs | 31 ++++++ .../AdvancedDebuggerDisplay/TerminalToken.cs | 12 +++ source/AdvancedDebuggerDisplay/Token.cs | 16 +++ source/AdvancedDebuggerDisplayExtensions.cs | 84 ++++++++++++++++ 9 files changed, 341 insertions(+) create mode 100644 source/AdvancedDebuggerDisplay/EvaluatedToken.cs create mode 100644 source/AdvancedDebuggerDisplay/Members/Field.cs create mode 100644 source/AdvancedDebuggerDisplay/Members/MemberMap.cs create mode 100644 source/AdvancedDebuggerDisplay/Members/Method.cs create mode 100644 source/AdvancedDebuggerDisplay/Members/Property.cs create mode 100644 source/AdvancedDebuggerDisplay/TerminalToken.cs create mode 100644 source/AdvancedDebuggerDisplay/Token.cs create mode 100644 source/AdvancedDebuggerDisplayExtensions.cs diff --git a/ChaosUtil.Debug.csproj b/ChaosUtil.Debug.csproj index 37ec86c..a9117ea 100644 --- a/ChaosUtil.Debug.csproj +++ b/ChaosUtil.Debug.csproj @@ -23,6 +23,7 @@ + diff --git a/source/AdvancedDebuggerDisplay/EvaluatedToken.cs b/source/AdvancedDebuggerDisplay/EvaluatedToken.cs new file mode 100644 index 0000000..b69ab17 --- /dev/null +++ b/source/AdvancedDebuggerDisplay/EvaluatedToken.cs @@ -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); + } +} diff --git a/source/AdvancedDebuggerDisplay/Members/Field.cs b/source/AdvancedDebuggerDisplay/Members/Field.cs new file mode 100644 index 0000000..50549bb --- /dev/null +++ b/source/AdvancedDebuggerDisplay/Members/Field.cs @@ -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; + } + } +} diff --git a/source/AdvancedDebuggerDisplay/Members/MemberMap.cs b/source/AdvancedDebuggerDisplay/Members/MemberMap.cs new file mode 100644 index 0000000..0764aba --- /dev/null +++ b/source/AdvancedDebuggerDisplay/Members/MemberMap.cs @@ -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( + SysCol.Dictionary> memberMap, + Type type, + string memberName, + Func getMember + ) + where Member : class + { + SysCol.Dictionary typeMemberMap; + if (!memberMap.TryGetValue(type, out typeMemberMap)) + memberMap[type] = typeMemberMap = new SysCol.Dictionary(); + + Member member; + if (!typeMemberMap.TryGetValue(memberName, out member)) + typeMemberMap[memberName] = member = getMember(BindingFlags.Public | BindingFlags.Instance) ?? getMember(BindingFlags.NonPublic | BindingFlags.Instance); + + return member; + } + } +} diff --git a/source/AdvancedDebuggerDisplay/Members/Method.cs b/source/AdvancedDebuggerDisplay/Members/Method.cs new file mode 100644 index 0000000..45c117c --- /dev/null +++ b/source/AdvancedDebuggerDisplay/Members/Method.cs @@ -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.empty, Array.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.empty); + targetType = meth.ReturnType; + return true; + } + else + return false; + } + } +} diff --git a/source/AdvancedDebuggerDisplay/Members/Property.cs b/source/AdvancedDebuggerDisplay/Members/Property.cs new file mode 100644 index 0000000..0e855d4 --- /dev/null +++ b/source/AdvancedDebuggerDisplay/Members/Property.cs @@ -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; + } + } +} diff --git a/source/AdvancedDebuggerDisplay/TerminalToken.cs b/source/AdvancedDebuggerDisplay/TerminalToken.cs new file mode 100644 index 0000000..983c3b3 --- /dev/null +++ b/source/AdvancedDebuggerDisplay/TerminalToken.cs @@ -0,0 +1,12 @@ +namespace ChaosUtil.Debug.AdvancedDebuggerDisplay +{ + class TerminalToken : Token + { + public TerminalToken(string token) + : base(token) + { } + + public override string Value() + => token; + } +} diff --git a/source/AdvancedDebuggerDisplay/Token.cs b/source/AdvancedDebuggerDisplay/Token.cs new file mode 100644 index 0000000..a29c544 --- /dev/null +++ b/source/AdvancedDebuggerDisplay/Token.cs @@ -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(); + } +} diff --git a/source/AdvancedDebuggerDisplayExtensions.cs b/source/AdvancedDebuggerDisplayExtensions.cs new file mode 100644 index 0000000..adbce1a --- /dev/null +++ b/source/AdvancedDebuggerDisplayExtensions.cs @@ -0,0 +1,84 @@ +using ChaosUtil.Debug.AdvancedDebuggerDisplay; +using ChaosUtil.Reflection; +using System; +using System.Diagnostics; +using System.Linq; +using SysCol = System.Collections.Generic; + +/// Provides a way of evaluating debugger displays at runtime and constructing recursive debugger displays. +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 displayAttributes + = new SysCol.Dictionary(); + + /// + /// Returns a string representation of as displayed by the debugger. + /// If a is attached to , + /// it will be used to generate the debugger display. + /// Otherwise will be used as fallback. + /// + /// The type of the object to be displayed. + /// The object to be displayed. + public static string DebugDisplay(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(true).FirstOrDefault(); + + if (debuggerDisplay == null) + return obj?.ToString() ?? "null"; + + return string.Concat(Tokenize(obj, debuggerDisplay.Value).Select(Token.Evaluate)); + } + + static SysCol.IEnumerable 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; + } + } +} From e742e1cbfd17a1f2f63c3f2979b0cda44b74e484 Mon Sep 17 00:00:00 2001 From: InsanityCode <56584762+InsanityCode@users.noreply.github.com> Date: Sun, 7 Apr 2024 14:59:15 +0200 Subject: [PATCH 2/2] fix white space errors --- source/AdvancedDebuggerDisplay/EvaluatedToken.cs | 2 +- source/AdvancedDebuggerDisplay/Members/Field.cs | 2 +- source/AdvancedDebuggerDisplay/Members/MemberMap.cs | 2 +- source/AdvancedDebuggerDisplay/Members/Method.cs | 2 +- source/AdvancedDebuggerDisplay/Members/Property.cs | 2 +- source/AdvancedDebuggerDisplay/TerminalToken.cs | 2 +- source/AdvancedDebuggerDisplay/Token.cs | 2 +- source/AdvancedDebuggerDisplayExtensions.cs | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/source/AdvancedDebuggerDisplay/EvaluatedToken.cs b/source/AdvancedDebuggerDisplay/EvaluatedToken.cs index b69ab17..c7c0dac 100644 --- a/source/AdvancedDebuggerDisplay/EvaluatedToken.cs +++ b/source/AdvancedDebuggerDisplay/EvaluatedToken.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; namespace ChaosUtil.Debug.AdvancedDebuggerDisplay diff --git a/source/AdvancedDebuggerDisplay/Members/Field.cs b/source/AdvancedDebuggerDisplay/Members/Field.cs index 50549bb..50af031 100644 --- a/source/AdvancedDebuggerDisplay/Members/Field.cs +++ b/source/AdvancedDebuggerDisplay/Members/Field.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using FieldMap = System.Collections.Generic.Dictionary< System.Type, diff --git a/source/AdvancedDebuggerDisplay/Members/MemberMap.cs b/source/AdvancedDebuggerDisplay/Members/MemberMap.cs index 0764aba..bcf7be5 100644 --- a/source/AdvancedDebuggerDisplay/Members/MemberMap.cs +++ b/source/AdvancedDebuggerDisplay/Members/MemberMap.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using SysCol = System.Collections.Generic; diff --git a/source/AdvancedDebuggerDisplay/Members/Method.cs b/source/AdvancedDebuggerDisplay/Members/Method.cs index 45c117c..4f9a95a 100644 --- a/source/AdvancedDebuggerDisplay/Members/Method.cs +++ b/source/AdvancedDebuggerDisplay/Members/Method.cs @@ -1,4 +1,4 @@ -using ChaosUtil.Primitives; +using ChaosUtil.Primitives; using System; using System.Reflection; using MethodMap = System.Collections.Generic.Dictionary< diff --git a/source/AdvancedDebuggerDisplay/Members/Property.cs b/source/AdvancedDebuggerDisplay/Members/Property.cs index 0e855d4..0ef640a 100644 --- a/source/AdvancedDebuggerDisplay/Members/Property.cs +++ b/source/AdvancedDebuggerDisplay/Members/Property.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using PropertyMap = System.Collections.Generic.Dictionary< System.Type, diff --git a/source/AdvancedDebuggerDisplay/TerminalToken.cs b/source/AdvancedDebuggerDisplay/TerminalToken.cs index 983c3b3..21202ba 100644 --- a/source/AdvancedDebuggerDisplay/TerminalToken.cs +++ b/source/AdvancedDebuggerDisplay/TerminalToken.cs @@ -1,4 +1,4 @@ -namespace ChaosUtil.Debug.AdvancedDebuggerDisplay +namespace ChaosUtil.Debug.AdvancedDebuggerDisplay { class TerminalToken : Token { diff --git a/source/AdvancedDebuggerDisplay/Token.cs b/source/AdvancedDebuggerDisplay/Token.cs index a29c544..4f614f9 100644 --- a/source/AdvancedDebuggerDisplay/Token.cs +++ b/source/AdvancedDebuggerDisplay/Token.cs @@ -1,4 +1,4 @@ -namespace ChaosUtil.Debug.AdvancedDebuggerDisplay +namespace ChaosUtil.Debug.AdvancedDebuggerDisplay { abstract class Token { diff --git a/source/AdvancedDebuggerDisplayExtensions.cs b/source/AdvancedDebuggerDisplayExtensions.cs index adbce1a..a0e2330 100644 --- a/source/AdvancedDebuggerDisplayExtensions.cs +++ b/source/AdvancedDebuggerDisplayExtensions.cs @@ -1,4 +1,4 @@ -using ChaosUtil.Debug.AdvancedDebuggerDisplay; +using ChaosUtil.Debug.AdvancedDebuggerDisplay; using ChaosUtil.Reflection; using System; using System.Diagnostics; @@ -12,7 +12,7 @@ public static class AdvancedDebuggerDisplayExtensions static readonly SysCol.Dictionary displayAttributes = new SysCol.Dictionary(); - + /// /// Returns a string representation of as displayed by the debugger. /// If a is attached to ,