Skip to content

Commit

Permalink
Add Options.InteropOptions.BuildCallStackHandler (#1793)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Marko Lahma <[email protected]>
  • Loading branch information
scgm0 and lahma authored Oct 27, 2024
1 parent 8e3e320 commit 7ac9805
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 14 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<PackageVersion Include="NUnit" Version="4.2.2" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.6.0" />
<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="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="Test262Harness" Version="1.0.1" />
Expand Down
59 changes: 59 additions & 0 deletions Jint.Tests.PublicInterface/CallStackTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using Jint.Runtime;
using SourceMaps;

namespace Jint.Tests.PublicInterface;

public class CallStackTests
Expand Down Expand Up @@ -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<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"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="NodaTime" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
<PackageReference Include="SourceMaps" />
<PackageReference Include="System.Text.Json" Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net8.0'))" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
Expand Down
2 changes: 1 addition & 1 deletion Jint/Engine.Advanced.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public string StackTrace
return string.Empty;
}

return _engine.CallStack.BuildCallStackString(lastSyntaxElement.Location);
return _engine.CallStack.BuildCallStackString(_engine, lastSyntaxElement.Location);
}
}

Expand Down
2 changes: 1 addition & 1 deletion Jint/Native/Error/ErrorConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
10 changes: 10 additions & 0 deletions Jint/Options.Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ public static Options SetWrapObjectHandler(this Options options, Options.WrapObj
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>
/// Sets the type converter to use.
/// </summary>
Expand Down
9 changes: 9 additions & 0 deletions Jint/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public class Options

public delegate bool ExceptionHandlerDelegate(Exception exception);

public delegate string? BuildCallStackDelegate(string shortDescription, SourceLocation location, string[]? arguments);

/// <summary>
/// Execution constraints for the engine.
/// </summary>
Expand Down Expand Up @@ -297,6 +299,13 @@ public class InteropOptions
/// </summary>
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>
Expand Down
54 changes: 44 additions & 10 deletions Jint/Runtime/CallStack/JintCallStack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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(')');
}
Expand All @@ -156,14 +162,15 @@ 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
var index = _stack._size - 1 - excludeTop;
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--;
Expand All @@ -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--;
Expand All @@ -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;
}

/// <summary>
/// A version of <see cref="AstExtensions.GetKey"/> that cannot get into loop as we are already building a stack.
/// </summary>
Expand All @@ -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 "?";
Expand Down
4 changes: 2 additions & 2 deletions Jint/Runtime/JavaScriptException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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));
}
}
Expand Down

0 comments on commit 7ac9805

Please sign in to comment.