Bläddra i källkod

Add better debugger view support via debugger attributes (#1656)

Marko Lahma 1 år sedan
förälder
incheckning
adeb772d49

+ 68 - 76
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<object>(Console.WriteLine))
+    .SetValue("load", new Func<string, object>(
+        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<object>(Console.WriteLine))
-                .SetValue("load", new Func<string, object>(
-                    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);
     }
 }

+ 33 - 0
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 @@ namespace Jint.Tests.Runtime
             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<int> { 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<JsValue, JsValue>();
+            dictionaryNet["name"] = "name";
+            dictionaryNet["age"] = 12;
+            dictionaryNet[GlobalSymbolRegistry.ToStringTag] = "Object";
+        }
+
         [Fact]
         public void CanAccessMemberNamedItemThroughExpando()
         {

+ 20 - 1
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
     /// <summary>
     /// Engine is the main API to JavaScript interpretation. Engine instances are not thread-safe.
     /// </summary>
+    [DebuggerTypeProxy(typeof(EngineDebugView))]
     public sealed partial class Engine : IDisposable
     {
         private static readonly Options _defaultEngineOptions = new();
@@ -1575,5 +1577,22 @@ namespace Jint
             clearMethod?.Invoke(_objectWrapperCache, Array.Empty<object>());
 #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;
+        }
     }
 }

+ 2 - 0
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 @@ using Jint.Runtime.Interpreter;
 
 namespace Jint.Native.Function
 {
+    [DebuggerDisplay("{ToString(),nq}")]
     public abstract partial class FunctionInstance : ObjectInstance, ICallable
     {
         protected PropertyDescriptor? _prototypeDescriptor;

+ 28 - 0
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
 {
     /// <summary>
@@ -21,4 +24,29 @@ public sealed class JsArray : ArrayInstance
     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;
+            }
+        }
+    }
 }

+ 3 - 0
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 @@ using Jint.Runtime;
 
 namespace Jint.Native;
 
+[DebuggerDisplay("{_value}", Type = "string")]
 public sealed class JsNumber : JsValue, IEquatable<JsNumber>
 {
     // .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

+ 3 - 0
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<JsString>, IEquatable<string>
 {
     private const int AsciiMax = 126;
@@ -28,6 +30,7 @@ public class JsString : JsValue, IEquatable<JsString>, IEquatable<string>
     internal static readonly JsString LengthString = new JsString("length");
     internal static readonly JsValue CommaString = new JsString(",");
 
+    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
     internal string _value;
 
     static JsString()

+ 7 - 48
Jint/Native/JsValue.cs

@@ -13,11 +13,12 @@ using Jint.Runtime.Interop;
 
 namespace Jint.Native
 {
-    [DebuggerTypeProxy(typeof(JsValueDebugView))]
     public abstract class JsValue : IEquatable<JsValue>
     {
         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 @@ namespace Jint.Native
         [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 @@ namespace Jint.Native
             return true;
         }
 
+        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
         public Types Type
         {
             [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -364,49 +368,6 @@ namespace Jint.Native
             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;
-                }
-            }
-        }
-
         /// <summary>
         /// Some values need to be cloned in order to be assigned, like ConcatenatedString.
         /// </summary>
@@ -419,11 +380,9 @@ namespace Jint.Native
                 : DoClone();
         }
 
-        internal virtual JsValue DoClone()
-        {
-            return this;
-        }
+        internal virtual JsValue DoClone() => this;
 
+        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
         internal virtual bool IsCallable => this is ICallable;
 
         /// <summary>

+ 46 - 0
Jint/Native/Object/ObjectInstance.cs

@@ -6,6 +6,7 @@ using Jint.Native.Array;
 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 @@ using Jint.Runtime.Interop;
 
 namespace Jint.Native.Object
 {
+    [DebuggerTypeProxy(typeof(ObjectInstanceDebugView))]
     public partial class ObjectInstance : JsValue, IEquatable<ObjectInstance>
     {
         private bool _initialized;
@@ -1200,6 +1202,7 @@ namespace Jint.Native.Object
 
         internal ICallable GetCallable(JsValue source) => source.GetCallable(_engine.Realm);
 
+        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
         internal bool IsConcatSpreadable
         {
             get
@@ -1213,15 +1216,18 @@ namespace Jint.Native.Object
             }
         }
 
+        [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 @@ namespace Jint.Native.Object
             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<JsValue, JsValue>[] Entries
+            {
+                get
+                {
+                    var keys = new KeyValuePair<JsValue, JsValue>[(_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<JsValue, JsValue>(key.Key.Name, UnwrapJsValue(key.Value, _obj));
+                        }
+                    }
+                    if (_obj._symbols is not null)
+                    {
+                        foreach(var key in _obj._symbols)
+                        {
+                            keys[i++] = new KeyValuePair<JsValue, JsValue>(key.Key, UnwrapJsValue(key.Value, _obj));
+                        }
+                    }
+                    return keys;
+                }
+            }
+
+            private string DebugToString() => new JsonSerializer(_obj._engine).Serialize(_obj, Undefined, "  ").ToString();
+        }
     }
 }

+ 27 - 0
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
     /// </summary>
+    [DebuggerTypeProxy(typeof(EnvironmentRecordDebugView))]
     public abstract class EnvironmentRecord : JsValue
     {
         protected internal readonly Engine _engine;
@@ -132,6 +133,32 @@ namespace Jint.Runtime.Environments
                 }
             }
         }
+
+        private sealed class EnvironmentRecordDebugView
+        {
+            private readonly EnvironmentRecord _record;
+
+            public EnvironmentRecordDebugView(EnvironmentRecord record)
+            {
+                _record = record;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public KeyValuePair<JsValue, JsValue>[] Entries
+            {
+                get
+                {
+                    var bindingNames = _record.GetAllBindingNames();
+                    var bindings = new KeyValuePair<JsValue, JsValue>[bindingNames.Length];
+                    var i = 0;
+                    foreach (var key in bindingNames)
+                    {
+                        bindings[i++] = new KeyValuePair<JsValue, JsValue>(key, _record.GetBindingValue(key, false));
+                    }
+                    return bindings;
+                }
+            }
+        }
     }
 }