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

Fix interop stack trace unwind (#1202) #1213

Merged
merged 2 commits into from
Jul 2, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 41 additions & 1 deletion Jint.Tests/Runtime/ErrorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,46 @@ public void StackTraceCollectedForImmediatelyInvokedFunctionExpression()
EqualIgnoringNewLineDifferences(expected, ex.GetJavaScriptErrorString());
}

// Verify #1202
[Fact]
public void StackIsUnwoundWhenExceptionHandledByInteropCode()
{
var engine = new Engine()
.SetValue("handle", new Action<Action>(Handler));

const string Script = @"
function throwIt(message) {
throw new Error(message);
}

handle(() => throwIt('e1'));
handle(() => throwIt('e2'));
handle(() => throwIt('e3'));

try {
throwIt('e4');
} catch(x){
x.stack; // return stack trace string
}
";
var stack = engine.Evaluate(Script).AsString();
EqualIgnoringNewLineDifferences(@" at Error <anonymous>:3:21
at throwIt (message) <anonymous>:3:15
at <anonymous>:11:5", stack);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this stack trace still isn't quite right, as the Error ctor should not be included. However, that is a separate issue (there are other tests that expect it to be there), which I may try to address in a subsequent PR.


static void Handler(Action callback)
{
try
{
callback();
}
catch (JavaScriptException)
{
// handle JS error
}
}
}

[Fact]
public void StackTraceIsForOriginalException()
{
Expand Down Expand Up @@ -327,4 +367,4 @@ private static void ContainsIgnoringNewLineDifferences(string expectedSubstring,
Assert.Contains(expectedSubstring, actualString);
}
}
}
}
24 changes: 18 additions & 6 deletions Jint/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1353,9 +1353,15 @@ ObjectInstance Callback()
ExceptionHelper.ThrowRecursionDepthOverflowException(CallStack, callStackElement.ToString());
}

var result = functionInstance.Call(thisObject, arguments);

CallStack.Pop();
JsValue result;
try
{
result = functionInstance.Call(thisObject, arguments);
}
finally
{
CallStack.Pop();
}

return result;
}
Expand All @@ -1376,9 +1382,15 @@ ObjectInstance Callback()
ExceptionHelper.ThrowRecursionDepthOverflowException(CallStack, callStackElement.ToString());
}

var result = ((IConstructor) functionInstance).Construct(arguments, newTarget);

CallStack.Pop();
ObjectInstance result;
try
{
result = ((IConstructor) functionInstance).Construct(arguments, newTarget);
}
finally
{
CallStack.Pop();
}

return result;
}
Expand Down
10 changes: 0 additions & 10 deletions Jint/Runtime/Interpreter/Statements/JintTryStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,6 @@ protected override Completion ExecuteInternal(EvaluationContext context)
// execute catch
if (_statement.Handler is not null)
{
// Quick-patch for call stack not being unwinded when an exception is caught.
// Ideally, this should instead be solved by always popping the stack when returning
// from a call, regardless of whether it throws (i.e. CallStack.Pop() in finally clause
// in Engine.Call/Engine.Construct - however, that method currently breaks stack traces
// in error messages.
while (callStackSizeBeforeExecution < engine.CallStack.Count)
{
engine.CallStack.Pop();
}

// https://tc39.es/ecma262/#sec-runtime-semantics-catchclauseevaluation

var thrownValue = b.Value;
Expand Down
37 changes: 24 additions & 13 deletions Jint/Runtime/JavaScriptException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ private static string GetMessage(JsValue error)
public string? JavaScriptStackTrace => _jsErrorException.StackTrace;
public Location Location => _jsErrorException.Location;
public JsValue Error => _jsErrorException.Error;

internal JavaScriptException(ErrorConstructor errorConstructor)
: base("", new JavaScriptErrorWrapperException(errorConstructor.Construct(Arguments.Empty), ""))
{
Expand All @@ -43,21 +43,21 @@ internal JavaScriptException(ErrorConstructor errorConstructor, string? message)
{
_jsErrorException = (JavaScriptErrorWrapperException) InnerException!;
}

internal JavaScriptException(JsValue error)
: base(GetMessage(error), new JavaScriptErrorWrapperException(error, GetMessage(error)))
{
_jsErrorException = (JavaScriptErrorWrapperException) InnerException!;
}

public string GetJavaScriptErrorString() => _jsErrorException.ToString();
public JavaScriptException SetJavaScriptCallstack(Engine engine, Location location)

public JavaScriptException SetJavaScriptCallstack(Engine engine, Location location, bool overwriteExisting = false)
{
_jsErrorException.SetCallstack(engine, location);
_jsErrorException.SetCallstack(engine, location, overwriteExisting);
return this;
}

public JavaScriptException SetJavaScriptLocation(Location location)
{
_jsErrorException.SetLocation(location);
Expand All @@ -82,15 +82,26 @@ internal void SetLocation(Location location)
Location = location;
}

internal void SetCallstack(Engine engine, Location location)
internal void SetCallstack(Engine engine, Location location, bool overwriteExisting)
{
Location = location;
var value = engine.CallStack.BuildCallStackString(location);
_callStack = value;
if (Error.IsObject())

var errObj = Error.IsObject() ? Error.AsObject() : null;
if (errObj == null)
{
_callStack = engine.CallStack.BuildCallStackString(location);
return;
}

// Does the Error object already have a stack property?
if (errObj.HasProperty(CommonProperties.Stack) && !overwriteExisting)
{
_callStack = errObj.Get(CommonProperties.Stack).AsString();
}
else
{
Error.AsObject()
.FastAddProperty(CommonProperties.Stack, new JsString(value), false, false, false);
_callStack = engine.CallStack.BuildCallStackString(location);
errObj.FastAddProperty(CommonProperties.Stack, _callStack, false, false, false);
}
}

Expand Down
3 changes: 2 additions & 1 deletion Jint/Runtime/TypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1030,7 +1030,8 @@ private static ObjectInstance ToObjectNonObject(Realm realm, JsValue value)
{
referencedName ??= "unknown";
var message = $"Cannot read property '{referencedName}' of {o}";
throw new JavaScriptException(engine.Realm.Intrinsics.TypeError, message).SetJavaScriptCallstack(engine, sourceNode.Location);
throw new JavaScriptException(engine.Realm.Intrinsics.TypeError, message)
.SetJavaScriptCallstack(engine, sourceNode.Location, overwriteExisting: true);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case we need to overwrite the existing stack trace (the one that is created within the Error ctor) in order to use the correct location (i.e. the passed source.Location).

}

public static void CheckObjectCoercible(Engine engine, JsValue o)
Expand Down