DumpNode.cs
#nullable enable
using System; using System.Linq; using System.Reflection; using System.Collections; using System.Diagnostics; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text.RegularExpressions; namespace Khooz.Show.Variable { [StructLayout(LayoutKind.Sequential)] public class DumpNode { private const byte MAX_DEPTH = 0xff; private const int MAX_VAL_LEN = 12; private const string OBS_OBJ = "@{...}"; private static readonly List<string> CACHE_TYPES = [ "RuntimeType", "GenericCache", "RuntimeTypeHandle", "RuntimeTypeCache", "RuntimeTypeInfo", "RuntimeTypeInfoCache", "System.RuntimeType", "System.GenericCache", "System.RuntimeTypeHandle", "System.RuntimeTypeCache", "System.RuntimeTypeInfo", "System.RuntimeTypeInfoCache", ]; public class Token(string text, ConsoleColor? color) { public string Text { get; } = text; public ConsoleColor Color { get; set; } = color ?? ConsoleColor.White; // Map ConsoleColor -> ANSI 30–37 codes static readonly Dictionary<ConsoleColor, int> _map = new() { [ConsoleColor.Black] = 30, [ConsoleColor.Red] = 31, [ConsoleColor.Green] = 32, [ConsoleColor.Yellow] = 33, [ConsoleColor.Blue] = 34, [ConsoleColor.Magenta] = 35, [ConsoleColor.Cyan] = 36, [ConsoleColor.White] = 37, // bright variants if desired: 90–97 }; public Token(string Text) : this(Text, null) { } public override string ToString() { int code = _map.ContainsKey(Color) ? _map[Color] : 0; return $"\u001b[{code}m{Text}\u001b[0m"; } public static void Write(Token t) { Console.ForegroundColor = t.Color; Console.Write(t.Text); Console.ResetColor(); } } [StructLayout(LayoutKind.Sequential)] public class Representation { public enum Category { _ = 0x00, PRIMITIVE = 0x01, BOOLEAN = 0x03, NUMBER = 0x05, STRING = 0x09, OBJECT = 0x10, ENUMERABLE = 0x30, COLLECTION = 0x70, DICTIONARY = 0xf0, FUNCTION = 0x100 }; public enum Accessor { _ = 0x00, PUBLIC = 0x01, PROTECTED = 0X02, PRIVATE = 0X04, INTERNAL = 0X08, PROTECTEDINTERNAL = 0X10, PRIVATEPROTECTED = 0X20, GETTER = 0x40, SETTER = 0x80, }; public enum Modifier { _ = 0X00, READONLY = 0X01, STATIC = 0X02, VIRTUAL = 0X04, ABSTRACT = 0X08, SEALED = 0X10, OVERRIDE = 0X20, ASYNC = 0X40, VOLATILE = 0X80 } public byte Accessors { get; set; } = 0; public byte Modifiers { get; set; } = 0; public string TypeName { get; set; } = "null"; public Category Cat { get; set; } = Category._; public string Name { get; set; } = ""; public string Value { get; set; } = ""; public Category GetCategory() { return TypeName switch { "System.Boolean" or "Boolean" => Category.BOOLEAN, "System.Byte" or "Byte" or "System.SByte" or "SByte" or "System.Char" or "Char" or "System.Int16" or "Int16" or "System.UInt16" or "UInt16" or "System.Int32" or "Int32" or "System.UInt32" or "UInt32" or "System.Int64" or "Int64" or "System.UInt64" or "UInt64" or "System.IntPtr" or "IntPtr" or "System.UIntPtr" or "UIntPtr" or "System.Single" or "Single" or "System.Double" or "Double" or "System.Decimal" or "Decimal" => Category.NUMBER, "String" or "String" => Category.STRING, _ when TypeName.StartsWith("IEnumerable") => Category.ENUMERABLE, _ when TypeName.StartsWith("IDictionary") => Category.DICTIONARY, _ when TypeName.StartsWith("Func") || TypeName.StartsWith("Action") => Category.FUNCTION, _ => Category.OBJECT }; } public override string ToString() { return ToString(false); } public string ToString(bool colorize) { var tn = TypeName; var n = Name; var v = Value; // var tn = TypeName; if (colorize) { tn = $"\u001b[34m{tn}\u001b[0m"; v = GetCategory() switch { Category.BOOLEAN => $"{TypeName} {Name} {Value}", Category.NUMBER => $"{TypeName} {Name} {Value}", Category.STRING => $"\u001b[31m\"{FormatValue(Value)}\u001b[0m\"", Category.ENUMERABLE or Category.DICTIONARY => Value, Category.FUNCTION => $"Func<{Value}>", _ => $"{TypeName} {Name} {FormatValue(Value)}", }; } return GetCategory() switch { Category.BOOLEAN => $"{tn} {n} {v}", Category.NUMBER => $"{tn} {n} {v}", Category.STRING => $"{tn} {n} {v}", Category.ENUMERABLE or Category.DICTIONARY => $"{tn} {n} {v}", Category.FUNCTION => $"v", _ => $"{tn} {n} {v}", }; } public List<Token> ToTokens(bool colorize) { Token tn = new(TypeName); Token n = new(Name); Token v = new(Value); // var tn = TypeName; if (colorize) { tn.Color = ConsoleColor.Blue; v.Color = GetCategory() switch { Category._ or Category.BOOLEAN=> ConsoleColor.Blue, Category.NUMBER => ConsoleColor.Green, Category.STRING => v.Text == "null" ? ConsoleColor.Blue : ConsoleColor.DarkYellow, Category.ENUMERABLE or Category.DICTIONARY => ConsoleColor.Gray, Category.FUNCTION => ConsoleColor.DarkMagenta, _ => v.Color, }; } return [tn, n, v]; } public override int GetHashCode() { return HashCode.Combine(Name, TypeName, Value); } public bool ReplaceInfo(Accessor access = Accessor._,Modifier mods = Modifier._) { Accessors = (byte)access; Modifiers = (byte)mods; return true; } public bool AddInfo(Accessor access = Accessor._,Modifier mods = Modifier._) { Accessors = (byte)(Accessors | (byte)access); Modifiers = (byte)(Modifiers | (byte)mods); return true; } public bool RemoveInfo(Accessor access = Accessor._,Modifier mods = Modifier._) { Accessors = (byte)(Accessors & ~(byte)access); Modifiers = (byte)(Modifiers & ~(byte)mods); return true; } } private readonly byte depth; // public object? Value { get; set; } public Representation Repr { get; } public List<DumpNode> Children { get; } public DumpNode(object? val) : this(val, null, 0, null) { } public DumpNode(object? val, Representation? repr) : this(val, repr, 0, null) { } public DumpNode(object? val, Representation? repr, byte depth) : this(val, repr, depth, null) { } public DumpNode(object? val, Representation? representation, byte depth, byte? maxDepth = null) { this.Repr = representation ?? new() { Name = "$ROOT", TypeName = val is null ? "null" : val.GetType().Name }; this.depth = depth; maxDepth ??= MAX_DEPTH; Children = []; // Console.WriteLine(Repr.ToString()); // return; // } // /* if (val?.GetType() == typeof(object)) return; if ( depth < maxDepth && val is not string && (!val?.GetType()?.IsPrimitive ?? false) ) { // Handle arrays and collections if (val is IEnumerable enumerable && val is not IDictionary) { var items = new List<object?>(); int idx = 0; foreach (var item in enumerable) { Representation i_repr = new() { Name = $"[{idx}]", TypeName = item?.GetType().Name ?? "null", }; Children.Add(new DumpNode(item, i_repr, (byte)(depth + 1), maxDepth)); items.Add(item); idx++; if (idx >= 30) break; // limit preview } Repr.Value = $"[{idx} items]"; } // Handle dictionaries else if (val is IDictionary dict) { var args = val?.GetType().GetGenericArguments() ?? []; Repr.TypeName = val?.GetType().Name ?? "Dictionary"; var tick = Repr.TypeName.IndexOf('`'); if (tick > 0) Repr.TypeName = Repr.TypeName[..tick]; Repr.TypeName = args.Length > 0 ? $"{Repr.TypeName}<{args?[0].Name ?? "K"},{args?[1].Name ?? "V"}>" : $"{Repr.TypeName}<Object?,Object?>"; int idx = 0; foreach (DictionaryEntry entry in dict) { Representation i_repr = new() { Name = args?.Length > 0 ? $"[{entry.Key}]" : $"[{entry.Key}.{entry.Key.GetHashCode()}]", TypeName = entry.Value?.GetType().Name ?? "null", }; Children.Add(new DumpNode(entry.Value, i_repr, (byte)(depth + 1), maxDepth)); idx++; if (idx >= 30) break; } Repr.Value = $"{{{idx} pairs}}"; } else { Repr.Value = "@{}"; Repr.TypeName = Repr.TypeName.Contains("AnonymousType") ? "AnonymousType" : Repr.TypeName; // Collect props, fields and methods for matching var props = val?.GetType() ?.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly) ?.ToDictionary(f => f.Name) ?? []; var fields = val?.GetType() ?.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly) ?.ToDictionary(f => PropertyName(f.Name)) ?? []; var methods = val?.GetType() ?.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly) ?? []; // Handle properties, merge with backing fields foreach (var prop in props.Values) { if ( (prop.DeclaringType != val?.GetType()) // && CACHE_TYPES.Contains(prop.GetType().Name) && IsSystemType(prop.GetType()) ) continue; // if (prop.GetIndexParameters().Length > 0) continue; // skip indexed properties // Try to find backing field string backingFieldName = fields.Keys.SingleOrDefault( _ => _.StartsWith($"<{prop.Name}>", StringComparison.OrdinalIgnoreCase), "" ); object childVal; try { childVal = prop.GetValue(val, null)!; } catch { continue; } // Build Repr var propType = prop.PropertyType.Name; var propName = prop.Name + (backingFieldName.Length > 0 ? $"({backingFieldName})" : ""); Representation i_repr = new() { Name = $"{prop.Name}", TypeName = prop.PropertyType.Name, Value = childVal is null ? "null" : childVal.ToString() ?? OBS_OBJ, }; // Describe accessors SetPropAttr(i_repr: ref i_repr, val: prop); Children.Add( new DumpNode(childVal, i_repr, (byte)(depth + 1), maxDepth)); } // Add fields not used as backing fields foreach ( var field in fields .Where(kv => !props.ContainsKey(kv.Key) || !kv.Value.Name.StartsWith($"<{props[kv.Key].Name}>")) .ToDictionary().Values ) { if ( (field.DeclaringType != val?.GetType()) // && CACHE_TYPES.Contains(field.GetType().Name) || IsSystemType(field) ) continue; object fieldVal; try { fieldVal = field.GetValue(val)!; } catch { continue; } Representation i_repr = new() { Name = $"{field.Name}", TypeName = field.FieldType.Name, Value = fieldVal is null ? "null" : fieldVal.ToString() ?? OBS_OBJ }; SetFieldAttr(i_repr: ref i_repr, val: field); Children.Add( new DumpNode(fieldVal, i_repr, (byte)(depth + 1), maxDepth)); } // Add method signatures (bound instance methods only) foreach (var method in methods) { if ( method.DeclaringType != val?.GetType() || IsSystemType(method.GetType()) || ( method.DeclaringType == typeof(object) && method.GetBaseDefinition() == method ) ) continue; if (method.IsSpecialName) continue; // skip property accessors, etc. // Build method signature string var parameters = method.GetParameters(); var paramList = parameters.Length > 0 ? string.Join(", ", parameters.Select(p => $"{p.ParameterType.Name} {p.Name}")) : ""; var signature = $"{method.ReturnType.Name} {method.Name}({paramList})"; Representation i_repr = new() { Name = $"{method.Name}", TypeName = $"Func<{method.ReturnType.Name} $RETURN{(paramList.Length > 0 ? $", {paramList}" : "")}>", Value = paramList, }; SetMethodAttr(i_repr: ref i_repr, val: method); Children.Add( new DumpNode(method, i_repr, (byte)(depth + 1), maxDepth)); } } } else { Repr.Value = val switch { null => "null", string s => s, bool b => b.ToString().ToLower(), char c => $"'{c}'", _ when val.GetType().IsPrimitive => val.ToString() ?? OBS_OBJ, _ => val.ToString() ?? OBS_OBJ }; } } // */ private static bool SetPropAttr(ref Representation i_repr, PropertyInfo val) { // Accessor detection var getMethod = val.GetGetMethod(true); var setMethod = val.GetSetMethod(true); bool is_set = false; is_set = getMethod != null && i_repr.AddInfo(access: Representation.Accessor.GETTER); is_set = getMethod != null && getMethod.IsPublic && i_repr.AddInfo(access: Representation.Accessor.PUBLIC); is_set = getMethod != null && getMethod.IsFamily && i_repr.AddInfo(access: Representation.Accessor.PROTECTED); is_set = getMethod != null && getMethod.IsPrivate && i_repr.AddInfo(access: Representation.Accessor.PRIVATE); is_set = getMethod != null && getMethod.IsAssembly && i_repr.AddInfo(access: Representation.Accessor.INTERNAL); is_set = getMethod != null && getMethod.IsFamilyOrAssembly && i_repr.AddInfo(access: Representation.Accessor.PROTECTEDINTERNAL); is_set = getMethod != null && getMethod.IsFamilyAndAssembly && i_repr.AddInfo(access: Representation.Accessor.PRIVATEPROTECTED); is_set = val.CanRead && (getMethod != null) && getMethod.IsStatic && i_repr.AddInfo(mods: Representation.Modifier.STATIC); is_set = val.CanRead && (getMethod != null) && getMethod.IsVirtual && i_repr.AddInfo(mods: Representation.Modifier.VIRTUAL); is_set = val.CanRead && (getMethod != null) && getMethod.IsAbstract && i_repr.AddInfo(mods: Representation.Modifier.ABSTRACT); is_set = val.CanRead && (getMethod != null) && getMethod.IsFinal && i_repr.AddInfo(mods: Representation.Modifier.SEALED); i_repr.AddInfo(access: Representation.Accessor.SETTER); is_set = setMethod != null && setMethod.IsPublic && i_repr.AddInfo(access: Representation.Accessor.PUBLIC); is_set = setMethod != null && setMethod.IsFamily && i_repr.AddInfo(access: Representation.Accessor.PROTECTED); is_set = setMethod != null && setMethod.IsPrivate && i_repr.AddInfo(access: Representation.Accessor.PRIVATE); is_set = setMethod != null && setMethod.IsAssembly && i_repr.AddInfo(access: Representation.Accessor.INTERNAL); is_set = setMethod != null && setMethod.IsFamilyOrAssembly && i_repr.AddInfo(access: Representation.Accessor.PROTECTEDINTERNAL); is_set = setMethod != null && setMethod.IsFamilyAndAssembly && i_repr.AddInfo(access: Representation.Accessor.PRIVATEPROTECTED); is_set = val.CanWrite && setMethod != null && setMethod.IsStatic && i_repr.AddInfo(mods: Representation.Modifier.STATIC); is_set = val.CanWrite && setMethod != null && setMethod.IsVirtual && i_repr.AddInfo(mods: Representation.Modifier.VIRTUAL); is_set = val.CanWrite && setMethod != null && setMethod.IsAbstract && i_repr.AddInfo(mods: Representation.Modifier.ABSTRACT); is_set = val.CanWrite && setMethod != null && setMethod.IsFinal && i_repr.AddInfo(mods: Representation.Modifier.SEALED); // Readonly detection (auto-property with getter only) is_set = val.CanRead && !val.CanWrite && i_repr.AddInfo(mods: Representation.Modifier.READONLY); return is_set; } private static bool SetFieldAttr(ref Representation i_repr, FieldInfo val) { // Accessor detection bool is_set = false; i_repr.AddInfo(access: Representation.Accessor.GETTER); is_set = val.IsPublic && i_repr.AddInfo(access: Representation.Accessor.PUBLIC); is_set = val.IsFamily && i_repr.AddInfo(access: Representation.Accessor.PROTECTED); is_set = val.IsPrivate && i_repr.AddInfo(access: Representation.Accessor.PRIVATE); is_set = val.IsAssembly && i_repr.AddInfo(access: Representation.Accessor.INTERNAL); is_set = val.IsFamilyOrAssembly && i_repr.AddInfo(access: Representation.Accessor.PROTECTEDINTERNAL); is_set = val.IsFamilyAndAssembly && i_repr.AddInfo(access: Representation.Accessor.PRIVATEPROTECTED); // readonly detection (auto-property with getter only) is_set = val.IsInitOnly && i_repr.AddInfo(mods: Representation.Modifier.READONLY); is_set = val.IsLiteral && i_repr.AddInfo(mods: Representation.Modifier.READONLY); // static detection is_set = val.IsStatic && i_repr.AddInfo(mods: Representation.Modifier.STATIC); return is_set; } private static bool SetMethodAttr(ref Representation i_repr, MethodInfo val) { // Accessor detection bool is_set = false; i_repr.AddInfo(access: Representation.Accessor.GETTER); is_set = val.IsPublic && i_repr.AddInfo(access: Representation.Accessor.PUBLIC); is_set = val.IsFamily && i_repr.AddInfo(access: Representation.Accessor.PROTECTED); is_set = val.IsPrivate && i_repr.AddInfo(access: Representation.Accessor.PRIVATE); is_set = val.IsAssembly && i_repr.AddInfo(access: Representation.Accessor.INTERNAL); is_set = val.IsFamilyOrAssembly && i_repr.AddInfo(access: Representation.Accessor.PROTECTEDINTERNAL); is_set = val.IsFamilyAndAssembly && i_repr.AddInfo(access: Representation.Accessor.PRIVATEPROTECTED); // readonly detection (auto-property with getter only) is_set = val.IsStatic && i_repr.AddInfo(mods: Representation.Modifier.STATIC); is_set = val.IsVirtual && i_repr.AddInfo(mods: Representation.Modifier.VIRTUAL); is_set = val.IsAbstract && i_repr.AddInfo(mods: Representation.Modifier.ABSTRACT); is_set = val.IsFinal && i_repr.AddInfo(mods: Representation.Modifier.SEALED); return is_set; } private static string PropertyName(string backingFieldName) { Match match = new Regex("<(.*?)>.*").Match(backingFieldName); return match.Success ? match.Groups[1].Value : backingFieldName; } private static bool IsSystemType(MemberInfo member) { return member.DeclaringType == typeof(object) || member.GetType().Name.StartsWith("System.", StringComparison.OrdinalIgnoreCase) || member.GetType().Name.StartsWith("Runtime", StringComparison.OrdinalIgnoreCase) || member.GetType().Name.StartsWith("Generic", StringComparison.OrdinalIgnoreCase); } private static string FormatValue(object? val) { if (val is null) return "null"; // Strings: quote + truncate if (val is string s) { var quoted = $"\"{s}\""; if (quoted.Length <= MAX_VAL_LEN) return quoted; return $"\"{s[..(MAX_VAL_LEN - 3)]}...\""; } // Booleans & chars if (val is bool b) return b.ToString().ToLower(); if (val is char c) return $"'{c}'"; // Numeric types: use general or scientific, then truncate if (val is byte || val is short || val is int || val is long || val is float || val is double || val is decimal) { double d = Convert.ToDouble(val); string str = d.ToString("G6"); if (str.Length > MAX_VAL_LEN) str = d.ToString("E4"); if (str.Length > MAX_VAL_LEN) str = $"{str[..(MAX_VAL_LEN - 3)]}..."; return str; } // Fallback to .ToString() + truncate var txt = val.ToString() ?? ""; if (txt.Length <= MAX_VAL_LEN) return txt; return $"{txt[..(MAX_VAL_LEN - 3)]}..."; } public void PrintTree() => PrintTree(true, "", true); public void PrintTree(bool color = true) => PrintTree(color, "", true); public void PrintTree(bool color = true, string indent = "") => PrintTree(color, indent, true); private void PrintTree(bool color = true, string indent = "", bool last = true) { var branch = last ? "└── " : "├── "; // use ValueType.Name or FullName here // var typeLabel = Value?.GetType()?.Name ?? "null"; Console.Write($"{indent}{branch}"); // foreach (var t in Repr.ToTokens(true)) t.Write(); Repr.ToTokens(true).ForEach(_ => { Console.Write(" "); Token.Write(_); }); Console.WriteLine(); var childIndent = indent + (last ? " " : "│ "); if (Children.Count > 0) for (int i = 0; i < Children.Count; i++) Children[i].PrintTree(color, childIndent, i == Children.Count - 1); } } } |