Просмотр исходного кода

Add Options.InteropOptions.BuildCallStackHandler (#1793)

---------

Co-authored-by: Marko Lahma <[email protected]>
神麤詭末 9 месяцев назад
Родитель
Сommit
7ac9805af1

+ 1 - 0
Directory.Packages.props

@@ -22,6 +22,7 @@
     <PackageVersion Include="NUnit" Version="4.2.2" />
     <PackageVersion Include="NUnit" Version="4.2.2" />
     <PackageVersion Include="NUnit3TestAdapter" Version="4.6.0" />
     <PackageVersion Include="NUnit3TestAdapter" Version="4.6.0" />
     <PackageVersion Include="SharpZipLib" Version="1.4.2" />
     <PackageVersion Include="SharpZipLib" Version="1.4.2" />
+    <PackageVersion Include="SourceMaps" Version="0.3.0" />
     <PackageVersion Include="Spectre.Console.Cli" Version="0.45.0" />
     <PackageVersion Include="Spectre.Console.Cli" Version="0.45.0" />
     <PackageVersion Include="System.Text.Json" Version="8.0.5" />
     <PackageVersion Include="System.Text.Json" Version="8.0.5" />
     <PackageVersion Include="Test262Harness" Version="1.0.1" />
     <PackageVersion Include="Test262Harness" Version="1.0.1" />

+ 59 - 0
Jint.Tests.PublicInterface/CallStackTests.cs

@@ -1,3 +1,6 @@
+using Jint.Runtime;
+using SourceMaps;
+
 namespace Jint.Tests.PublicInterface;
 namespace Jint.Tests.PublicInterface;
 
 
 public class CallStackTests
 public class CallStackTests
@@ -46,4 +49,60 @@ Trace
             _output.WriteLine($"Trace{Environment.NewLine}{_engine.Advanced.StackTrace}");
             _output.WriteLine($"Trace{Environment.NewLine}{_engine.Advanced.StackTrace}");
         }
         }
     }
     }
+
+    [Fact]
+    public void ShouldReturnTheSourceMapStack()
+    {
+        var sourceMap = SourceMapParser.Parse("""{"version":3,"file":"custom.js","sourceRoot":"","sources":["custom.ts"],"names":[],"mappings":"AAEA,SAAS,CAAC,CAAC,CAAM;IAChB,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC;AAED,IAAI,CAAC,GAAG,UAAU,CAAM;IACvB,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AACb,CAAC,CAAA;AAED,CAAC,CAAC,CAAC,CAAC,CAAC"}""");
+
+        string BuildCallStackHandler(string description, SourceLocation location, string[] arguments)
+        {
+            if (location.SourceFile != sourceMap.File)
+            {
+                return null;
+            }
+
+            var originalPosition = sourceMap.OriginalPositionFor(location.End.Line, location.Start.Column + 1);
+
+            if (originalPosition is null)
+            {
+                return null;
+            }
+
+            var str = $"   at{
+                (!string.IsNullOrWhiteSpace(description) ? $" {description}" : "")
+            } {
+                originalPosition.Value.OriginalFileName
+            }:{
+                originalPosition.Value.OriginalLineNumber + 1
+            }:{
+                originalPosition.Value.OriginalColumnNumber
+            }{
+                Environment.NewLine
+            }";
+
+            return str;
+        }
+
+        var engine = new Engine(opt =>
+        {
+            opt.SetBuildCallStackHandler(BuildCallStackHandler);
+        });
+
+        const string Script = @"function a(v) {
+    throw new Error(v);
+}
+var b = function (v) {
+    return a(v);
+};
+b(7);
+//# sourceMappingURL=custom.js.map";
+        var ex = Assert.Throws<JavaScriptException>(() => engine.Execute(Script, "custom.js"));
+
+        var stack = ex.JavaScriptStackTrace!;
+        Assert.Equal(@"   at a custom.ts:4:7
+   at b custom.ts:8:9
+   at custom.ts:11:1".Replace("\r\n", "\n"), stack.Replace("\r\n", "\n"));
+    }
+
 }
 }

+ 1 - 0
Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj

@@ -21,6 +21,7 @@
     <PackageReference Include="Newtonsoft.Json" />
     <PackageReference Include="Newtonsoft.Json" />
     <PackageReference Include="NodaTime" />
     <PackageReference Include="NodaTime" />
     <PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
     <PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
+    <PackageReference Include="SourceMaps" />
     <PackageReference Include="System.Text.Json" Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net8.0'))" />
     <PackageReference Include="System.Text.Json" Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net8.0'))" />
     <PackageReference Include="xunit" />
     <PackageReference Include="xunit" />
     <PackageReference Include="xunit.runner.visualstudio" />
     <PackageReference Include="xunit.runner.visualstudio" />

+ 1 - 1
Jint/Engine.Advanced.cs

@@ -28,7 +28,7 @@ public partial class Engine
                     return string.Empty;
                     return string.Empty;
                 }
                 }
 
 
-                return _engine.CallStack.BuildCallStackString(lastSyntaxElement.Location);
+                return _engine.CallStack.BuildCallStackString(_engine, lastSyntaxElement.Location);
             }
             }
         }
         }
 
 

+ 1 - 1
Jint/Native/Error/ErrorConstructor.cs

@@ -80,7 +80,7 @@ public sealed class ErrorConstructor : Constructor
 
 
             // If the current function is the ErrorConstructor itself (i.e. "throw new Error(...)" was called
             // If the current function is the ErrorConstructor itself (i.e. "throw new Error(...)" was called
             // from script), exclude it from the stack trace, because the trace should begin at the throw point.
             // from script), exclude it from the stack trace, because the trace should begin at the throw point.
-            return callStack.BuildCallStackString(lastSyntaxNode.Location, currentFunction == this ? 1 : 0);
+            return callStack.BuildCallStackString(_engine, lastSyntaxNode.Location, currentFunction == this ? 1 : 0);
         }
         }
     }
     }
 }
 }

+ 10 - 0
Jint/Options.Extensions.cs

@@ -127,6 +127,16 @@ public static class OptionsExtensions
         return options;
         return options;
     }
     }
 
 
+    /// <summary>
+    /// Sets the handler used to build stack traces. This is useful if the code currently
+    /// running was transpiled (eg. TypeScript) and the source map of original code is available.
+    /// </summary>
+    public static Options SetBuildCallStackHandler(this Options options, Options.BuildCallStackDelegate buildCallStackHandler)
+    {
+        options.Interop.BuildCallStackHandler = buildCallStackHandler;
+        return options;
+    }
+
     /// <summary>
     /// <summary>
     /// Sets the type converter to use.
     /// Sets the type converter to use.
     /// </summary>
     /// </summary>

+ 9 - 0
Jint/Options.cs

@@ -27,6 +27,8 @@ public class Options
 
 
     public delegate bool ExceptionHandlerDelegate(Exception exception);
     public delegate bool ExceptionHandlerDelegate(Exception exception);
 
 
+    public delegate string? BuildCallStackDelegate(string shortDescription, SourceLocation location, string[]? arguments);
+
     /// <summary>
     /// <summary>
     /// Execution constraints for the engine.
     /// Execution constraints for the engine.
     /// </summary>
     /// </summary>
@@ -297,6 +299,13 @@ public class Options
         /// </summary>
         /// </summary>
         public WrapObjectDelegate WrapObjectHandler { get; set; } = static (engine, target, type) => ObjectWrapper.Create(engine, target, type);
         public WrapObjectDelegate WrapObjectHandler { get; set; } = static (engine, target, type) => ObjectWrapper.Create(engine, target, type);
 
 
+        /// <summary>
+        /// The handler used to build stack traces. Changing this enables mapping
+        /// stack traces to code different from the code being executed, eg. when
+        /// executing code transpiled from TypeScript.
+        /// </summary>
+        public BuildCallStackDelegate? BuildCallStackHandler { get; set; }
+
         /// <summary>
         /// <summary>
         ///
         ///
         /// </summary>
         /// </summary>

+ 44 - 10
Jint/Runtime/CallStack/JintCallStack.cs

@@ -114,14 +114,20 @@ internal sealed class JintCallStack
         return string.Join("->", _stack.Select(static cse => cse.ToString()).Reverse());
         return string.Join("->", _stack.Select(static cse => cse.ToString()).Reverse());
     }
     }
 
 
-    internal string BuildCallStackString(SourceLocation location, int excludeTop = 0)
+    internal string BuildCallStackString(Engine engine, SourceLocation location, int excludeTop = 0)
     {
     {
         static void AppendLocation(
         static void AppendLocation(
             ref ValueStringBuilder sb,
             ref ValueStringBuilder sb,
             string shortDescription,
             string shortDescription,
             in SourceLocation loc,
             in SourceLocation loc,
-            in CallStackElement? element)
+            in CallStackElement? element,
+            Options.BuildCallStackDelegate? callStackBuilder)
         {
         {
+            if (callStackBuilder != null && TryInvokeCustomCallStackHandler(callStackBuilder, element, shortDescription, loc, ref sb))
+            {
+                return;
+            }
+
             sb.Append("   at");
             sb.Append("   at");
 
 
             if (!string.IsNullOrWhiteSpace(shortDescription))
             if (!string.IsNullOrWhiteSpace(shortDescription))
@@ -134,15 +140,15 @@ internal sealed class JintCallStack
             {
             {
                 // it's a function
                 // it's a function
                 sb.Append(" (");
                 sb.Append(" (");
-                for (var index = 0; index < element.Value.Arguments.Value.Count; index++)
+                var arguments = element.Value.Arguments.Value;
+                for (var i = 0; i < arguments.Count; i++)
                 {
                 {
-                    if (index != 0)
+                    if (i != 0)
                     {
                     {
                         sb.Append(", ");
                         sb.Append(", ");
                     }
                     }
 
 
-                    var arg = element.Value.Arguments.Value[index];
-                    sb.Append(GetPropertyKey(arg));
+                    sb.Append(GetPropertyKey(arguments[i]));
                 }
                 }
                 sb.Append(')');
                 sb.Append(')');
             }
             }
@@ -156,6 +162,7 @@ internal sealed class JintCallStack
             sb.Append(System.Environment.NewLine);
             sb.Append(System.Environment.NewLine);
         }
         }
 
 
+        var customCallStackBuilder = engine.Options.Interop.BuildCallStackHandler;
         var builder = new ValueStringBuilder();
         var builder = new ValueStringBuilder();
 
 
         // stack is one frame behind function-wise when we start to process it from expression level
         // stack is one frame behind function-wise when we start to process it from expression level
@@ -163,7 +170,7 @@ internal sealed class JintCallStack
         var element = index >= 0 ? _stack[index] : (CallStackElement?) null;
         var element = index >= 0 ? _stack[index] : (CallStackElement?) null;
         var shortDescription = element?.ToString() ?? "";
         var shortDescription = element?.ToString() ?? "";
 
 
-        AppendLocation(ref builder, shortDescription, location, element);
+        AppendLocation(ref builder, shortDescription, location, element, customCallStackBuilder);
 
 
         location = element?.Location ?? default;
         location = element?.Location ?? default;
         index--;
         index--;
@@ -173,7 +180,7 @@ internal sealed class JintCallStack
             element = index >= 0 ? _stack[index] : null;
             element = index >= 0 ? _stack[index] : null;
             shortDescription = element?.ToString() ?? "";
             shortDescription = element?.ToString() ?? "";
 
 
-            AppendLocation(ref builder, shortDescription, location, element);
+            AppendLocation(ref builder, shortDescription, location, element, customCallStackBuilder);
 
 
             location = element?.Location ?? default;
             location = element?.Location ?? default;
             index--;
             index--;
@@ -186,6 +193,34 @@ internal sealed class JintCallStack
         return result;
         return result;
     }
     }
 
 
+    private static bool TryInvokeCustomCallStackHandler(
+        Options.BuildCallStackDelegate handler,
+        CallStackElement? element,
+        string shortDescription,
+        SourceLocation loc,
+        ref ValueStringBuilder sb)
+    {
+        string[]? arguments = null;
+        if (element?.Arguments is not null)
+        {
+            var args = element.Value.Arguments.Value;
+            arguments = args.Count > 0 ? new string[args.Count] : [];
+            for (var i = 0; i < arguments.Length; i++)
+            {
+                arguments[i] = GetPropertyKey(args[i]);
+            }
+        }
+
+        var str = handler(shortDescription, loc, arguments);
+        if (!string.IsNullOrEmpty(str))
+        {
+            sb.Append(str);
+            return true;
+        }
+
+        return false;
+    }
+
     /// <summary>
     /// <summary>
     /// A version of <see cref="AstExtensions.GetKey"/> that cannot get into loop as we are already building a stack.
     /// A version of <see cref="AstExtensions.GetKey"/> that cannot get into loop as we are already building a stack.
     /// </summary>
     /// </summary>
@@ -203,8 +238,7 @@ internal sealed class JintCallStack
 
 
         if (expression is MemberExpression { Computed: false } staticMemberExpression)
         if (expression is MemberExpression { Computed: false } staticMemberExpression)
         {
         {
-            return GetPropertyKey(staticMemberExpression.Object) + "." +
-                   GetPropertyKey(staticMemberExpression.Property);
+            return $"{GetPropertyKey(staticMemberExpression.Object)}.{GetPropertyKey(staticMemberExpression.Property)}";
         }
         }
 
 
         return "?";
         return "?";

+ 2 - 2
Jint/Runtime/JavaScriptException.cs

@@ -88,7 +88,7 @@ public class JavaScriptException : JintException
             var errObj = Error.IsObject() ? Error.AsObject() : null;
             var errObj = Error.IsObject() ? Error.AsObject() : null;
             if (errObj is null)
             if (errObj is null)
             {
             {
-                _callStack = engine.CallStack.BuildCallStackString(location);
+                _callStack = engine.CallStack.BuildCallStackString(engine, location);
                 return;
                 return;
             }
             }
 
 
@@ -99,7 +99,7 @@ public class JavaScriptException : JintException
             }
             }
             else
             else
             {
             {
-                _callStack = engine.CallStack.BuildCallStackString(location);
+                _callStack = engine.CallStack.BuildCallStackString(engine, location);
                 errObj.FastSetProperty(CommonProperties.Stack._value, new PropertyDescriptor(_callStack, false, false, false));
                 errObj.FastSetProperty(CommonProperties.Stack._value, new PropertyDescriptor(_callStack, false, false, false));
             }
             }
         }
         }