Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Options.InteropOptions.BuildCallStackHandler #1793

Merged
merged 2 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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