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)); } }