Skip to content

Commit 01e624d

Browse files
committed
In JavaScriptEngineSwitcher.Jint runtime exceptions now contain a stack trace
1 parent 773cd72 commit 01e624d

File tree

7 files changed

+198
-49
lines changed

7 files changed

+198
-49
lines changed
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Text;
5+
using System.Text.RegularExpressions;
6+
7+
using AdvancedStringBuilder;
8+
9+
using JavaScriptEngineSwitcher.Core.Extensions;
10+
using JavaScriptEngineSwitcher.Core.Helpers;
11+
12+
using CoreStrings = JavaScriptEngineSwitcher.Core.Resources.Strings;
13+
14+
namespace JavaScriptEngineSwitcher.Jint.Helpers
15+
{
16+
/// <summary>
17+
/// JS error helpers
18+
/// </summary>
19+
internal static class JintJsErrorHelpers
20+
{
21+
#region Error location
22+
23+
private const string OriginalAnonymousFunctionName = "(anonymous)";
24+
private const string WrapperAnonymousFunctionName = "Anonymous function";
25+
26+
/// <summary>
27+
/// Regular expression for working with line of the script error location
28+
/// </summary>
29+
private static readonly Regex _errorLocationLineRegex =
30+
new Regex(@"^[ ]{3}at " +
31+
@"(?:" +
32+
@"(?<functionName>" +
33+
@"[\w][\w ]*" +
34+
@"|" +
35+
CommonRegExps.JsFullNamePattern +
36+
@"|" +
37+
Regex.Escape(OriginalAnonymousFunctionName) +
38+
@") " +
39+
@"(?:\(" + CommonRegExps.JsFullNamePattern + @"(?:, " + CommonRegExps.JsFullNamePattern + @")*\) )?" +
40+
@")?" +
41+
@"(?<documentName>" + CommonRegExps.DocumentNamePattern + @"):" +
42+
@"(?<lineNumber>\d+)(?::(?<columnNumber>\d+))?$");
43+
44+
45+
/// <summary>
46+
/// Parses a string representation of the script error location to produce an array of
47+
/// <see cref="ErrorLocationItem"/> instances
48+
/// </summary>
49+
/// <param name="errorLocation">String representation of the script error location</param>
50+
/// <returns>An array of <see cref="ErrorLocationItem"/> instances</returns>
51+
public static ErrorLocationItem[] ParseErrorLocation(string errorLocation)
52+
{
53+
if (string.IsNullOrWhiteSpace(errorLocation))
54+
{
55+
return new ErrorLocationItem[0];
56+
}
57+
58+
var errorLocationItems = new List<ErrorLocationItem>();
59+
string[] lines = errorLocation.SplitToLines();
60+
int lineCount = lines.Length;
61+
62+
for (int lineIndex = 0; lineIndex < lineCount; lineIndex++)
63+
{
64+
string line = lines[lineIndex];
65+
Match lineMatch = _errorLocationLineRegex.Match(line);
66+
67+
if (lineMatch.Success)
68+
{
69+
GroupCollection lineGroups = lineMatch.Groups;
70+
71+
var errorLocationItem = new ErrorLocationItem
72+
{
73+
FunctionName = lineGroups["functionName"].Value,
74+
DocumentName = lineGroups["documentName"].Value,
75+
LineNumber = int.Parse(lineGroups["lineNumber"].Value),
76+
ColumnNumber = lineGroups["columnNumber"].Success ?
77+
int.Parse(lineGroups["columnNumber"].Value) : 0
78+
};
79+
errorLocationItems.Add(errorLocationItem);
80+
}
81+
else
82+
{
83+
Debug.WriteLine(string.Format(CoreStrings.Runtime_InvalidErrorLocationLineFormat, line));
84+
return new ErrorLocationItem[0];
85+
}
86+
}
87+
88+
return errorLocationItems.ToArray();
89+
}
90+
91+
/// <summary>
92+
/// Fixes a error location items
93+
/// </summary>
94+
/// <param name="errorLocationItems">An array of <see cref="ErrorLocationItem"/> instances</param>
95+
/// <param name="currentDocumentName">Current document name</param>
96+
public static void FixErrorLocationItems(ErrorLocationItem[] errorLocationItems, string currentDocumentName)
97+
{
98+
foreach (ErrorLocationItem errorLocationItem in errorLocationItems)
99+
{
100+
string functionName = errorLocationItem.FunctionName;
101+
if (functionName.Length > 0)
102+
{
103+
if (functionName == OriginalAnonymousFunctionName)
104+
{
105+
errorLocationItem.FunctionName = WrapperAnonymousFunctionName;
106+
}
107+
}
108+
else
109+
{
110+
errorLocationItem.FunctionName = "Global code";
111+
}
112+
113+
if (errorLocationItem.DocumentName == ":0" && errorLocationItem.LineNumber == 1)
114+
{
115+
errorLocationItem.DocumentName = currentDocumentName;
116+
errorLocationItem.LineNumber = 0;
117+
errorLocationItem.ColumnNumber = 0;
118+
}
119+
}
120+
}
121+
122+
/// <summary>
123+
/// Converts a call chain to stack
124+
/// </summary>
125+
/// <param name="callChain">Call chain</param>
126+
/// <returns>Call stack</returns>
127+
public static string ConvertCallChainToStack(string callChain)
128+
{
129+
string callStack = string.Empty;
130+
string[] callChainItems = callChain
131+
.Split(new string[] { "->" }, StringSplitOptions.None)
132+
;
133+
134+
if (callChainItems.Length > 0)
135+
{
136+
var stringBuilderPool = StringBuilderPool.Shared;
137+
StringBuilder stackBuilder = stringBuilderPool.Rent();
138+
139+
for (int chainItemIndex = callChainItems.Length - 1; chainItemIndex >= 0; chainItemIndex--)
140+
{
141+
string chainItem = callChainItems[chainItemIndex];
142+
if (chainItem == OriginalAnonymousFunctionName)
143+
{
144+
chainItem = WrapperAnonymousFunctionName;
145+
}
146+
147+
JsErrorHelpers.WriteErrorLocationLine(stackBuilder, chainItem, string.Empty, 0, 0);
148+
if (chainItemIndex > 0)
149+
{
150+
stackBuilder.AppendLine();
151+
}
152+
}
153+
154+
callStack = stackBuilder.ToString();
155+
stringBuilderPool.Return(stackBuilder);
156+
}
157+
158+
return callStack;
159+
}
160+
161+
#endregion
162+
}
163+
}

src/JavaScriptEngineSwitcher.Jint/JavaScriptEngineSwitcher.Jint.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
<Description>JavaScriptEngineSwitcher.Jint contains adapter `JintJsEngine` (wrapper for the Jint JavaScript Engine (http://github.com/sebastienros/jint) version 3.0.0 Beta 2002).</Description>
1414
<PackageIcon>icon.png</PackageIcon>
1515
<PackageTags>JavaScriptEngineSwitcher;JavaScript;ECMAScript;Jint</PackageTags>
16-
<PackageReleaseNotes>1. Jint was updated to version 3.0.0 Beta 2002;
17-
2. In configuration settings of the Jint JS engine a `AllowDebuggerStatement` property has been replaced by the `DebuggerStatementHandlingMode` property (default `Ignore`) and was added two new properties: `DebuggerBreakCallback` (default `null`) and `DebuggerStepCallback` (default `null`).</PackageReleaseNotes>
16+
<PackageReleaseNotes>Runtime exceptions now contain a stack trace.</PackageReleaseNotes>
1817
</PropertyGroup>
1918

2019
<Import Project="../../build/common.props" />

src/JavaScriptEngineSwitcher.Jint/JintJsEngine.cs

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
using System;
2-
using System.Text;
32
using System.Threading;
43

54
using Jint;
65
using IOriginalPrimitiveInstance = Jint.Native.IPrimitiveInstance;
6+
using OriginalCancellationConstraint = Jint.Constraints.CancellationConstraint;
77
using OriginalDebuggerBreakDelegate = Jint.Engine.BreakDelegate;
88
using OriginalDebuggerStatementHandlingMode = Jint.Runtime.Debugger.DebuggerStatementHandling;
99
using OriginalDebuggerStepDelegate = Jint.Engine.DebugStepDelegate;
10-
using OriginalCancellationConstraint = Jint.Constraints.CancellationConstraint;
1110
using OriginalEngine = Jint.Engine;
1211
using OriginalExecutionCanceledException = Jint.Runtime.ExecutionCanceledException;
1312
using OriginalJavaScriptException = Jint.Runtime.JavaScriptException;
@@ -24,8 +23,6 @@
2423
using OriginalTypes = Jint.Runtime.Types;
2524
using OriginalValue = Jint.Native.JsValue;
2625

27-
using AdvancedStringBuilder;
28-
2926
using JavaScriptEngineSwitcher.Core;
3027
using JavaScriptEngineSwitcher.Core.Constants;
3128
using JavaScriptEngineSwitcher.Core.Helpers;
@@ -38,6 +35,8 @@
3835
using WrapperTimeoutException = JavaScriptEngineSwitcher.Core.JsTimeoutException;
3936
using WrapperUsageException = JavaScriptEngineSwitcher.Core.JsUsageException;
4037

38+
using JavaScriptEngineSwitcher.Jint.Helpers;
39+
4140
namespace JavaScriptEngineSwitcher.Jint
4241
{
4342
/// <summary>
@@ -252,6 +251,14 @@ private WrapperRuntimeException WrapRuntimeException(OriginalRuntimeException or
252251
lineNumber = originalJavaScriptException.LineNumber;
253252
columnNumber = originalJavaScriptException.Column + 1;
254253

254+
ErrorLocationItem[] callStackItems = JintJsErrorHelpers.ParseErrorLocation(
255+
originalJavaScriptException.StackTrace);
256+
if (callStackItems.Length > 0)
257+
{
258+
JintJsErrorHelpers.FixErrorLocationItems(callStackItems, documentName);
259+
callStack = JsErrorHelpers.StringifyErrorLocationItems(callStackItems, true);
260+
}
261+
255262
OriginalValue errorValue = originalJavaScriptException.Error;
256263
if (errorValue.IsObject())
257264
{
@@ -269,8 +276,7 @@ private WrapperRuntimeException WrapRuntimeException(OriginalRuntimeException or
269276
type = JsErrorType.Common;
270277
}
271278

272-
message = JsErrorHelpers.GenerateScriptErrorMessage(type, description, documentName, lineNumber,
273-
columnNumber);
279+
message = JsErrorHelpers.GenerateScriptErrorMessage(type, description, callStack);
274280

275281
wrapperRuntimeException = new WrapperRuntimeException(message, EngineName, EngineVersion,
276282
originalJavaScriptException);
@@ -286,34 +292,7 @@ private WrapperRuntimeException WrapRuntimeException(OriginalRuntimeException or
286292
else if (originalRuntimeException is OriginalRecursionDepthOverflowException)
287293
{
288294
var originalRecursionException = (OriginalRecursionDepthOverflowException)originalRuntimeException;
289-
string[] callChainItems = originalRecursionException.CallChain
290-
.Split(new string[] { "->" }, StringSplitOptions.None)
291-
;
292-
293-
if (callChainItems.Length > 0)
294-
{
295-
var stringBuilderPool = StringBuilderPool.Shared;
296-
StringBuilder stackBuilder = stringBuilderPool.Rent();
297-
298-
for (int chainItemIndex = callChainItems.Length - 1; chainItemIndex >= 0; chainItemIndex--)
299-
{
300-
string chainItem = callChainItems[chainItemIndex];
301-
if (chainItem == "(anonymous)")
302-
{
303-
chainItem = "Anonymous function";
304-
}
305-
306-
JsErrorHelpers.WriteErrorLocationLine(stackBuilder, chainItem, string.Empty, 0, 0);
307-
if (chainItemIndex > 0)
308-
{
309-
stackBuilder.AppendLine();
310-
}
311-
}
312-
313-
callStack = stackBuilder.ToString();
314-
stringBuilderPool.Return(stackBuilder);
315-
}
316-
295+
callStack = JintJsErrorHelpers.ConvertCallChainToStack(originalRecursionException.CallChain);
317296
type = JsErrorType.Range;
318297
message = JsErrorHelpers.GenerateScriptErrorMessage(type, description, callStack);
319298

src/JavaScriptEngineSwitcher.Jint/readme.txt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,7 @@
1818
=============
1919
RELEASE NOTES
2020
=============
21-
1. Jint was updated to version 3.0.0 Beta 2002;
22-
2. In configuration settings of the Jint JS engine a `AllowDebuggerStatement`
23-
property has been replaced by the `DebuggerStatementHandlingMode` property
24-
(default `Ignore`) and was added two new properties: `DebuggerBreakCallback`
25-
(default `null`) and `DebuggerStepCallback` (default `null`).
21+
Runtime exceptions now contain a stack trace.
2622

2723
=============
2824
DOCUMENTATION

test/JavaScriptEngineSwitcher.Tests/Jint/CommonTests.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ protected override string EngineName
1515
get { return "JintJsEngine"; }
1616
}
1717

18+
1819
#region Error handling
1920

2021
#region Mapping of errors
@@ -89,7 +90,7 @@ public void MappingRuntimeErrorDuringEvaluationOfExpressionIsCorrect()
8990
Assert.Equal(5, exception.LineNumber);
9091
Assert.Equal(1, exception.ColumnNumber);
9192
Assert.Empty(exception.SourceFragment);
92-
Assert.Empty(exception.CallStack);
93+
Assert.Equal(" at Global code (variables.js:5:1)", exception.CallStack);
9394
}
9495

9596
[Fact]
@@ -174,7 +175,11 @@ public void MappingRuntimeErrorDuringExecutionOfCodeIsCorrect()
174175
Assert.Equal(3, exception.LineNumber);
175176
Assert.Equal(3, exception.ColumnNumber);
176177
Assert.Empty(exception.SourceFragment);
177-
Assert.Empty(exception.CallStack);
178+
Assert.Equal(
179+
" at factorial (factorial.js:3:3)" + Environment.NewLine +
180+
" at Global code (factorial.js:10:1)",
181+
exception.CallStack
182+
);
178183
}
179184

180185
[Fact]
@@ -447,7 +452,9 @@ public void GenerationOfRuntimeErrorMessageIsCorrect()
447452
foo(a, b);
448453
})(foo);";
449454
string targetOutput = "ReferenceError: bar is not defined" + Environment.NewLine +
450-
" at functions.js:4:3"
455+
" at foo (functions.js:4:3)" + Environment.NewLine +
456+
" at Anonymous function (functions.js:12:2)" + Environment.NewLine +
457+
" at Global code (functions.js)"
451458
;
452459

453460
JsRuntimeException exception = null;

test/JavaScriptEngineSwitcher.Tests/Jint/InteropTests.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,9 +298,7 @@ public void MappingRuntimeErrorDuringRecursiveExecutionOfFilesIsCorrect()
298298
Assert.Equal(1, exception.LineNumber);
299299
Assert.Equal(1, exception.ColumnNumber);
300300
Assert.Equal("", exception.SourceFragment);
301-
Assert.Equal(
302-
"",
303-
exception.CallStack
301+
Assert.Equal(" at Global code (second-file.js:1:1)", exception.CallStack
304302
);
305303
}
306304

test/JavaScriptEngineSwitcher.Tests/Jint/PrecompilationTests.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,12 @@ public void MappingRuntimeErrorDuringExecutionOfPrecompiledCodeIsCorrect()
107107
Assert.Equal(2, exception.LineNumber);
108108
Assert.Equal(19, exception.ColumnNumber);
109109
Assert.Empty(exception.SourceFragment);
110-
Assert.Empty(exception.CallStack);
110+
Assert.Equal(
111+
" at getItem (get-item.js:2:19)" + Environment.NewLine +
112+
" at Anonymous function (get-item.js:9:10)" + Environment.NewLine +
113+
" at Global code (get-item.js)",
114+
exception.CallStack
115+
);
111116
}
112117

113118
#endregion
@@ -173,7 +178,9 @@ public void GenerationOfRuntimeErrorMessageIsCorrect()
173178
return getFullName(firstName, lastName);
174179
})(getFullName);";
175180
string targetOutput = "ReferenceError: middleName is not defined" + Environment.NewLine +
176-
" at get-full-name.js:2:2"
181+
" at getFullName (get-full-name.js:2:2)" + Environment.NewLine +
182+
" at Anonymous function (get-full-name.js:12:9)" + Environment.NewLine +
183+
" at Global code (get-full-name.js)"
177184
;
178185

179186
JsRuntimeException exception = null;

0 commit comments

Comments
 (0)