From adeb772d4984095f78a787dea8a3a459ed13a803 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 23 Oct 2023 20:23:30 +0300 Subject: [PATCH] Add better debugger view support via debugger attributes (#1656) --- Jint.Repl/Program.cs | 144 +++++++++--------- Jint.Tests/Runtime/InteropTests.Dynamic.cs | 33 ++++ Jint/Engine.cs | 21 ++- Jint/Native/Function/FunctionInstance.cs | 2 + Jint/Native/JsArray.cs | 28 ++++ Jint/Native/JsNumber.cs | 3 + Jint/Native/JsString.cs | 3 + Jint/Native/JsValue.cs | 55 +------ Jint/Native/Object/ObjectInstance.cs | 46 ++++++ .../Runtime/Environments/EnvironmentRecord.cs | 27 ++++ 10 files changed, 237 insertions(+), 125 deletions(-) diff --git a/Jint.Repl/Program.cs b/Jint.Repl/Program.cs index d52bdfbd41..fda7d8dc71 100644 --- a/Jint.Repl/Program.cs +++ b/Jint.Repl/Program.cs @@ -1,97 +1,89 @@ using System.Diagnostics; using System.Reflection; using Esprima; +using Jint; using Jint.Native; using Jint.Native.Json; using Jint.Runtime; -namespace Jint.Repl +var engine = new Engine(cfg => cfg + .AllowClr() +); + +engine + .SetValue("print", new Action(Console.WriteLine)) + .SetValue("load", new Func( + path => engine.Evaluate(File.ReadAllText(path))) + ); + +var filename = args.Length > 0 ? args[0] : ""; +if (!string.IsNullOrEmpty(filename)) { - internal static class Program + if (!File.Exists(filename)) { - private static void Main(string[] args) - { - var engine = new Engine(cfg => cfg - .AllowClr() - ); - - engine - .SetValue("print", new Action(Console.WriteLine)) - .SetValue("load", new Func( - path => engine.Evaluate(File.ReadAllText(path))) - ); + Console.WriteLine("Could not find file: {0}", filename); + } - var filename = args.Length > 0 ? args[0] : ""; - if (!string.IsNullOrEmpty(filename)) - { - if (!File.Exists(filename)) - { - Console.WriteLine("Could not find file: {0}", filename); - } + var script = File.ReadAllText(filename); + engine.Evaluate(script, "repl"); + return; +} - var script = File.ReadAllText(filename); - engine.Evaluate(script, "repl"); - return; - } +var assembly = Assembly.GetExecutingAssembly(); +var fvi = FileVersionInfo.GetVersionInfo(assembly.Location); +var version = fvi.FileVersion; - var assembly = Assembly.GetExecutingAssembly(); - var fvi = FileVersionInfo.GetVersionInfo(assembly.Location); - var version = fvi.FileVersion; +Console.WriteLine("Welcome to Jint ({0})", version); +Console.WriteLine("Type 'exit' to leave, " + + "'print()' to write on the console, " + + "'load()' to load scripts."); +Console.WriteLine(); - Console.WriteLine("Welcome to Jint ({0})", version); - Console.WriteLine("Type 'exit' to leave, " + - "'print()' to write on the console, " + - "'load()' to load scripts."); - Console.WriteLine(); +var defaultColor = Console.ForegroundColor; +var parserOptions = new ParserOptions +{ + Tolerant = true, + RegExpParseMode = RegExpParseMode.AdaptToInterpreted +}; - var defaultColor = Console.ForegroundColor; - var parserOptions = new ParserOptions - { - Tolerant = true, - RegExpParseMode = RegExpParseMode.AdaptToInterpreted - }; +var serializer = new JsonSerializer(engine); - var serializer = new JsonSerializer(engine); +while (true) +{ + Console.ForegroundColor = defaultColor; + Console.Write("jint> "); + var input = Console.ReadLine(); + if (input is "exit" or ".exit") + { + return; + } - while (true) + try + { + var result = engine.Evaluate(input, parserOptions); + JsValue str = result; + if (!result.IsPrimitive() && result is not IPrimitiveInstance) + { + str = serializer.Serialize(result, JsValue.Undefined, " "); + if (str == JsValue.Undefined) { - Console.ForegroundColor = defaultColor; - Console.Write("jint> "); - var input = Console.ReadLine(); - if (input is "exit" or ".exit") - { - return; - } - - try - { - var result = engine.Evaluate(input, parserOptions); - JsValue str = result; - if (!result.IsPrimitive() && result is not IPrimitiveInstance) - { - str = serializer.Serialize(result, JsValue.Undefined, " "); - if (str == JsValue.Undefined) - { - str = result; - } - } - else if (result.IsString()) - { - str = serializer.Serialize(result, JsValue.Undefined, JsValue.Undefined); - } - Console.WriteLine(str); - } - catch (JavaScriptException je) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(je.ToString()); - } - catch (Exception e) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(e.Message); - } + str = result; } } + else if (result.IsString()) + { + str = serializer.Serialize(result, JsValue.Undefined, JsValue.Undefined); + } + Console.WriteLine(str); + } + catch (JavaScriptException je) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(je.ToString()); + } + catch (Exception e) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(e.Message); } } diff --git a/Jint.Tests/Runtime/InteropTests.Dynamic.cs b/Jint.Tests/Runtime/InteropTests.Dynamic.cs index d8563d6502..d0de054323 100644 --- a/Jint.Tests/Runtime/InteropTests.Dynamic.cs +++ b/Jint.Tests/Runtime/InteropTests.Dynamic.cs @@ -1,4 +1,7 @@ using System.Dynamic; +using Jint.Native; +using Jint.Native.Symbol; +using Jint.Tests.Runtime.Domain; namespace Jint.Tests.Runtime { @@ -14,6 +17,36 @@ public void CanAccessExpandoObject() Assert.Equal("test", engine.Evaluate("expando.Name").ToString()); } + [Fact] + public void DebugView() + { + // allows displaying different local variables under debugger + + var engine = new Engine(); + var boolNet = true; + var boolJint = (JsBoolean) boolNet; + var doubleNet = 12.34; + var doubleJint = (JsNumber) doubleNet; + var integerNet = 42; + var integerJint = (JsNumber) integerNet; + var stringNet = "ABC"; + var stringJint = (JsString) stringNet; + var arrayNet = new[] { 1, 2, 3 }; + var arrayListNet = new List { 1, 2, 3 }; + var arrayJint = new JsArray(engine, arrayNet.Select(x => (JsNumber) x).ToArray()); + + var objectNet = new Person { Name = "name", Age = 12 }; + var objectJint = new JsObject(engine); + objectJint["name"] = "name"; + objectJint["age"] = 12; + objectJint[GlobalSymbolRegistry.ToStringTag] = "Object"; + + var dictionaryNet = new Dictionary(); + dictionaryNet["name"] = "name"; + dictionaryNet["age"] = 12; + dictionaryNet[GlobalSymbolRegistry.ToStringTag] = "Object"; + } + [Fact] public void CanAccessMemberNamedItemThroughExpando() { diff --git a/Jint/Engine.cs b/Jint/Engine.cs index be39814a89..cfa2399b20 100644 --- a/Jint/Engine.cs +++ b/Jint/Engine.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using System.Diagnostics; +using System.Runtime.CompilerServices; using Esprima; using Esprima.Ast; using Jint.Native; @@ -24,6 +25,7 @@ namespace Jint /// /// Engine is the main API to JavaScript interpretation. Engine instances are not thread-safe. /// + [DebuggerTypeProxy(typeof(EngineDebugView))] public sealed partial class Engine : IDisposable { private static readonly Options _defaultEngineOptions = new(); @@ -1575,5 +1577,22 @@ public void Dispose() clearMethod?.Invoke(_objectWrapperCache, Array.Empty()); #endif } + + [DebuggerDisplay("Engine")] + private sealed class EngineDebugView + { + private readonly Engine _engine; + + public EngineDebugView(Engine engine) + { + _engine = engine; + } + + public ObjectInstance Globals => _engine.Realm.GlobalObject; + public Options Options => _engine.Options; + + public EnvironmentRecord VariableEnvironment => _engine.ExecutionContext.VariableEnvironment; + public EnvironmentRecord LexicalEnvironment => _engine.ExecutionContext.LexicalEnvironment; + } } } diff --git a/Jint/Native/Function/FunctionInstance.cs b/Jint/Native/Function/FunctionInstance.cs index 44ec6525ac..832536aff0 100644 --- a/Jint/Native/Function/FunctionInstance.cs +++ b/Jint/Native/Function/FunctionInstance.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Runtime.CompilerServices; using Esprima.Ast; using Jint.Native.Object; @@ -9,6 +10,7 @@ namespace Jint.Native.Function { + [DebuggerDisplay("{ToString(),nq}")] public abstract partial class FunctionInstance : ObjectInstance, ICallable { protected PropertyDescriptor? _prototypeDescriptor; diff --git a/Jint/Native/JsArray.cs b/Jint/Native/JsArray.cs index 26068a839b..7dc4473e78 100644 --- a/Jint/Native/JsArray.cs +++ b/Jint/Native/JsArray.cs @@ -1,7 +1,10 @@ +using System.Diagnostics; using Jint.Native.Array; namespace Jint.Native; +[DebuggerTypeProxy(typeof(JsArrayDebugView))] +[DebuggerDisplay("Count = {Length}")] public sealed class JsArray : ArrayInstance { /// @@ -21,4 +24,29 @@ public JsArray(Engine engine, uint capacity = 0, uint length = 0) : base(engine, public JsArray(Engine engine, JsValue[] items) : base(engine, items) { } + + private sealed class JsArrayDebugView + { + private readonly JsArray _array; + + public JsArrayDebugView(JsArray array) + { + _array = array; + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public JsValue[] Values + { + get + { + var values = new JsValue[_array.Length]; + var i = 0; + foreach (var value in _array) + { + values[i++] = value; + } + return values; + } + } + } } diff --git a/Jint/Native/JsNumber.cs b/Jint/Native/JsNumber.cs index 5306bd2b4e..df6267717f 100644 --- a/Jint/Native/JsNumber.cs +++ b/Jint/Native/JsNumber.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; using Jint.Native.Number; @@ -5,11 +6,13 @@ namespace Jint.Native; +[DebuggerDisplay("{_value}", Type = "string")] public sealed class JsNumber : JsValue, IEquatable { // .NET double epsilon and JS epsilon have different values internal const double JavaScriptEpsilon = 2.2204460492503130808472633361816E-16; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] internal readonly double _value; // how many decimals to check when determining if double is actually an int diff --git a/Jint/Native/JsString.cs b/Jint/Native/JsString.cs index b418ca5924..c9600b1997 100644 --- a/Jint/Native/JsString.cs +++ b/Jint/Native/JsString.cs @@ -1,8 +1,10 @@ +using System.Diagnostics; using System.Text; using Jint.Runtime; namespace Jint.Native; +[DebuggerDisplay("{ToString()}")] public class JsString : JsValue, IEquatable, IEquatable { private const int AsciiMax = 126; @@ -28,6 +30,7 @@ public class JsString : JsValue, IEquatable, IEquatable internal static readonly JsString LengthString = new JsString("length"); internal static readonly JsValue CommaString = new JsString(","); + [DebuggerBrowsable(DebuggerBrowsableState.Never)] internal string _value; static JsString() diff --git a/Jint/Native/JsValue.cs b/Jint/Native/JsValue.cs index 47e3f1ffcd..10c594f052 100644 --- a/Jint/Native/JsValue.cs +++ b/Jint/Native/JsValue.cs @@ -13,11 +13,12 @@ namespace Jint.Native { - [DebuggerTypeProxy(typeof(JsValueDebugView))] public abstract class JsValue : IEquatable { public static readonly JsValue Undefined = new JsUndefined(); public static readonly JsValue Null = new JsNull(); + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] internal readonly InternalTypes _type; protected JsValue(Types type) @@ -33,8 +34,10 @@ internal JsValue(InternalTypes type) [Pure] public virtual bool IsArray() => false; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] internal virtual bool IsIntegerIndexedArray => false; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] internal virtual bool IsConstructor => false; [Pure] @@ -99,6 +102,7 @@ internal bool TryGetIterator(Realm realm, [NotNullWhen(true)] out IteratorInstan return true; } + [DebuggerBrowsable(DebuggerBrowsableState.Never)] public Types Type { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -364,49 +368,6 @@ public override int GetHashCode() return _type.GetHashCode(); } - internal sealed class JsValueDebugView - { - public string Value; - - public JsValueDebugView(JsValue value) - { - switch (value.Type) - { - case Types.None: - Value = "None"; - break; - case Types.Undefined: - Value = "undefined"; - break; - case Types.Null: - Value = "null"; - break; - case Types.Boolean: - Value = ((JsBoolean) value)._value + " (bool)"; - break; - case Types.String: - Value = value.ToString() + " (string)"; - break; - case Types.Number: - Value = ((JsNumber) value)._value + " (number)"; - break; - case Types.BigInt: - Value = ((JsBigInt) value)._value + " (bigint)"; - break; - case Types.Object: - Value = value.AsObject().GetType().Name; - break; - case Types.Symbol: - var jsValue = ((JsSymbol) value)._value; - Value = (jsValue.IsUndefined() ? "" : jsValue.ToString()) + " (symbol)"; - break; - default: - Value = "Unknown"; - break; - } - } - } - /// /// Some values need to be cloned in order to be assigned, like ConcatenatedString. /// @@ -419,11 +380,9 @@ internal JsValue Clone() : DoClone(); } - internal virtual JsValue DoClone() - { - return this; - } + internal virtual JsValue DoClone() => this; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] internal virtual bool IsCallable => this is ICallable; /// diff --git a/Jint/Native/Object/ObjectInstance.cs b/Jint/Native/Object/ObjectInstance.cs index 4c7d7e8735..0942141081 100644 --- a/Jint/Native/Object/ObjectInstance.cs +++ b/Jint/Native/Object/ObjectInstance.cs @@ -6,6 +6,7 @@ using Jint.Native.BigInt; using Jint.Native.Boolean; using Jint.Native.Function; +using Jint.Native.Json; using Jint.Native.Number; using Jint.Native.RegExp; using Jint.Native.String; @@ -17,6 +18,7 @@ namespace Jint.Native.Object { + [DebuggerTypeProxy(typeof(ObjectInstanceDebugView))] public partial class ObjectInstance : JsValue, IEquatable { private bool _initialized; @@ -1200,6 +1202,7 @@ bool TryGetValue(ulong idx, out JsValue jsValue) internal ICallable GetCallable(JsValue source) => source.GetCallable(_engine.Realm); + [DebuggerBrowsable(DebuggerBrowsableState.Never)] internal bool IsConcatSpreadable { get @@ -1213,15 +1216,18 @@ internal bool IsConcatSpreadable } } + [DebuggerBrowsable(DebuggerBrowsableState.Never)] public virtual bool IsArrayLike => TryGetValue(CommonProperties.Length, out var lengthValue) && lengthValue.IsNumber() && ((JsNumber) lengthValue)._value >= 0; // safe default + [DebuggerBrowsable(DebuggerBrowsableState.Never)] internal virtual bool HasOriginalIterator => false; internal override bool IsIntegerIndexedArray => false; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] public virtual uint Length => (uint) TypeConverter.ToLength(Get(CommonProperties.Length)); public virtual bool PreventExtensions() @@ -1649,5 +1655,45 @@ internal enum IntegrityLevel Sealed, Frozen } + + private sealed class ObjectInstanceDebugView + { + private readonly ObjectInstance _obj; + + public ObjectInstanceDebugView(ObjectInstance obj) + { + _obj = obj; + } + + public ObjectInstance? Prototype => _obj.Prototype; + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public KeyValuePair[] Entries + { + get + { + var keys = new KeyValuePair[(_obj._properties?.Count ?? 0) + (_obj._symbols?.Count ?? 0)]; + + var i = 0; + if (_obj._properties is not null) + { + foreach(var key in _obj._properties) + { + keys[i++] = new KeyValuePair(key.Key.Name, UnwrapJsValue(key.Value, _obj)); + } + } + if (_obj._symbols is not null) + { + foreach(var key in _obj._symbols) + { + keys[i++] = new KeyValuePair(key.Key, UnwrapJsValue(key.Value, _obj)); + } + } + return keys; + } + } + + private string DebugToString() => new JsonSerializer(_obj._engine).Serialize(_obj, Undefined, " ").ToString(); + } } } diff --git a/Jint/Runtime/Environments/EnvironmentRecord.cs b/Jint/Runtime/Environments/EnvironmentRecord.cs index 143a5ebaf4..299c13d804 100644 --- a/Jint/Runtime/Environments/EnvironmentRecord.cs +++ b/Jint/Runtime/Environments/EnvironmentRecord.cs @@ -8,6 +8,7 @@ namespace Jint.Runtime.Environments /// Base implementation of an Environment Record /// https://tc39.es/ecma262/#sec-environment-records /// + [DebuggerTypeProxy(typeof(EnvironmentRecordDebugView))] public abstract class EnvironmentRecord : JsValue { protected internal readonly Engine _engine; @@ -132,6 +133,32 @@ public BindingName(string value) } } } + + private sealed class EnvironmentRecordDebugView + { + private readonly EnvironmentRecord _record; + + public EnvironmentRecordDebugView(EnvironmentRecord record) + { + _record = record; + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public KeyValuePair[] Entries + { + get + { + var bindingNames = _record.GetAllBindingNames(); + var bindings = new KeyValuePair[bindingNames.Length]; + var i = 0; + foreach (var key in bindingNames) + { + bindings[i++] = new KeyValuePair(key, _record.GetBindingValue(key, false)); + } + return bindings; + } + } + } } }