diff --git a/Directory.Packages.props b/Directory.Packages.props
index e83742915..b910d8d27 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -22,6 +22,7 @@
+
diff --git a/Jint.Tests.PublicInterface/CallStackTests.cs b/Jint.Tests.PublicInterface/CallStackTests.cs
index 959681aff..bce0af8fa 100644
--- a/Jint.Tests.PublicInterface/CallStackTests.cs
+++ b/Jint.Tests.PublicInterface/CallStackTests.cs
@@ -1,3 +1,6 @@
+using Jint.Runtime;
+using SourceMaps;
+
namespace Jint.Tests.PublicInterface;
public class CallStackTests
@@ -46,4 +49,60 @@ public void Trace()
_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(() => 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"));
+ }
+
}
diff --git a/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj b/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj
index 06e26b9cf..cfd17efdb 100644
--- a/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj
+++ b/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj
@@ -21,6 +21,7 @@
+
diff --git a/Jint/Engine.Advanced.cs b/Jint/Engine.Advanced.cs
index 61b8672a0..2bd8aace3 100644
--- a/Jint/Engine.Advanced.cs
+++ b/Jint/Engine.Advanced.cs
@@ -28,7 +28,7 @@ public string StackTrace
return string.Empty;
}
- return _engine.CallStack.BuildCallStackString(lastSyntaxElement.Location);
+ return _engine.CallStack.BuildCallStackString(_engine, lastSyntaxElement.Location);
}
}
diff --git a/Jint/Native/Error/ErrorConstructor.cs b/Jint/Native/Error/ErrorConstructor.cs
index 518f59d10..563d4dbc5 100644
--- a/Jint/Native/Error/ErrorConstructor.cs
+++ b/Jint/Native/Error/ErrorConstructor.cs
@@ -80,7 +80,7 @@ public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget)
// 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.
- return callStack.BuildCallStackString(lastSyntaxNode.Location, currentFunction == this ? 1 : 0);
+ return callStack.BuildCallStackString(_engine, lastSyntaxNode.Location, currentFunction == this ? 1 : 0);
}
}
}
diff --git a/Jint/Options.Extensions.cs b/Jint/Options.Extensions.cs
index 2ca151241..ee9143738 100644
--- a/Jint/Options.Extensions.cs
+++ b/Jint/Options.Extensions.cs
@@ -127,6 +127,16 @@ public static Options SetWrapObjectHandler(this Options options, Options.WrapObj
return options;
}
+ ///
+ /// 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.
+ ///
+ public static Options SetBuildCallStackHandler(this Options options, Options.BuildCallStackDelegate buildCallStackHandler)
+ {
+ options.Interop.BuildCallStackHandler = buildCallStackHandler;
+ return options;
+ }
+
///
/// Sets the type converter to use.
///
diff --git a/Jint/Options.cs b/Jint/Options.cs
index 1969f0477..cdaf84162 100644
--- a/Jint/Options.cs
+++ b/Jint/Options.cs
@@ -27,6 +27,8 @@ public class Options
public delegate bool ExceptionHandlerDelegate(Exception exception);
+ public delegate string? BuildCallStackDelegate(string shortDescription, SourceLocation location, string[]? arguments);
+
///
/// Execution constraints for the engine.
///
@@ -297,6 +299,13 @@ public class InteropOptions
///
public WrapObjectDelegate WrapObjectHandler { get; set; } = static (engine, target, type) => ObjectWrapper.Create(engine, target, type);
+ ///
+ /// 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.
+ ///
+ public BuildCallStackDelegate? BuildCallStackHandler { get; set; }
+
///
///
///
diff --git a/Jint/Runtime/CallStack/JintCallStack.cs b/Jint/Runtime/CallStack/JintCallStack.cs
index b52fedaa8..116f08b86 100644
--- a/Jint/Runtime/CallStack/JintCallStack.cs
+++ b/Jint/Runtime/CallStack/JintCallStack.cs
@@ -114,14 +114,20 @@ public override string ToString()
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(
ref ValueStringBuilder sb,
string shortDescription,
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");
if (!string.IsNullOrWhiteSpace(shortDescription))
@@ -134,15 +140,15 @@ static void AppendLocation(
{
// it's a function
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(", ");
}
- var arg = element.Value.Arguments.Value[index];
- sb.Append(GetPropertyKey(arg));
+ sb.Append(GetPropertyKey(arguments[i]));
}
sb.Append(')');
}
@@ -156,6 +162,7 @@ static void AppendLocation(
sb.Append(System.Environment.NewLine);
}
+ var customCallStackBuilder = engine.Options.Interop.BuildCallStackHandler;
var builder = new ValueStringBuilder();
// stack is one frame behind function-wise when we start to process it from expression level
@@ -163,7 +170,7 @@ static void AppendLocation(
var element = index >= 0 ? _stack[index] : (CallStackElement?) null;
var shortDescription = element?.ToString() ?? "";
- AppendLocation(ref builder, shortDescription, location, element);
+ AppendLocation(ref builder, shortDescription, location, element, customCallStackBuilder);
location = element?.Location ?? default;
index--;
@@ -173,7 +180,7 @@ static void AppendLocation(
element = index >= 0 ? _stack[index] : null;
shortDescription = element?.ToString() ?? "";
- AppendLocation(ref builder, shortDescription, location, element);
+ AppendLocation(ref builder, shortDescription, location, element, customCallStackBuilder);
location = element?.Location ?? default;
index--;
@@ -186,6 +193,34 @@ static void AppendLocation(
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;
+ }
+
///
/// A version of that cannot get into loop as we are already building a stack.
///
@@ -203,8 +238,7 @@ private static string GetPropertyKey(Node expression)
if (expression is MemberExpression { Computed: false } staticMemberExpression)
{
- return GetPropertyKey(staticMemberExpression.Object) + "." +
- GetPropertyKey(staticMemberExpression.Property);
+ return $"{GetPropertyKey(staticMemberExpression.Object)}.{GetPropertyKey(staticMemberExpression.Property)}";
}
return "?";
diff --git a/Jint/Runtime/JavaScriptException.cs b/Jint/Runtime/JavaScriptException.cs
index 7f407a9d5..cd50d8b55 100644
--- a/Jint/Runtime/JavaScriptException.cs
+++ b/Jint/Runtime/JavaScriptException.cs
@@ -88,7 +88,7 @@ internal void SetCallstack(Engine engine, in SourceLocation location, bool overw
var errObj = Error.IsObject() ? Error.AsObject() : null;
if (errObj is null)
{
- _callStack = engine.CallStack.BuildCallStackString(location);
+ _callStack = engine.CallStack.BuildCallStackString(engine, location);
return;
}
@@ -99,7 +99,7 @@ internal void SetCallstack(Engine engine, in SourceLocation location, bool overw
}
else
{
- _callStack = engine.CallStack.BuildCallStackString(location);
+ _callStack = engine.CallStack.BuildCallStackString(engine, location);
errObj.FastSetProperty(CommonProperties.Stack._value, new PropertyDescriptor(_callStack, false, false, false));
}
}