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

Ensure script preparation exposes only Jint's exception type #1927

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

namespace Jint.Tests.Runtime;

public partial class EngineTests
public class ScriptModulePreparationTests
{
[Fact]
public void ScriptPreparationAcceptsReturnOutsideOfFunctions()
{
var preparedScript = Engine.PrepareScript("return 1;");
Assert.IsType<ReturnStatement>(preparedScript.Program.Body[0]);
preparedScript.Program.Body[0].Should().BeOfType<ReturnStatement>();
}

[Fact]
Expand All @@ -21,10 +21,10 @@ public void CanPreCompileRegex()
var script = Engine.PrepareScript("var x = /[cgt]/ig; var y = /[cgt]/ig; 'g'.match(x).length;");
var declaration = Assert.IsType<VariableDeclaration>(script.Program.Body[0]);
var init = Assert.IsType<RegExpLiteral>(declaration.Declarations[0].Init);
Assert.Equal("[cgt]", init.Value.ToString());
Assert.Equal(RegexOptions.Compiled, init.Value.Options & RegexOptions.Compiled);

Assert.Equal(1, _engine.Evaluate(script));
init.Value.ToString().Should().Be("[cgt]");
(init.Value.Options & RegexOptions.Compiled).Should().Be(RegexOptions.Compiled);
new Engine().Evaluate(script).AsNumber().Should().Be(1);
}

[Fact]
Expand All @@ -33,9 +33,9 @@ public void ScriptPreparationFoldsConstants()
var preparedScript = Engine.PrepareScript("return 1 + 2;");
var returnStatement = Assert.IsType<ReturnStatement>(preparedScript.Program.Body[0]);
var constant = Assert.IsType<JintConstantExpression>(returnStatement.Argument?.UserData);
Assert.Equal(3, constant.GetValue(null!));

Assert.Equal(3, _engine.Evaluate(preparedScript));
constant.GetValue(null!).AsNumber().Should().Be(3);
new Engine().Evaluate(preparedScript).AsNumber().Should().Be(3);
}

[Fact]
Expand All @@ -46,8 +46,8 @@ public void ScriptPreparationOptimizesNegatingUnaryExpression()
var unaryExpression = Assert.IsType<NonUpdateUnaryExpression>(expression.Expression);
var constant = Assert.IsType<JintConstantExpression>(unaryExpression.UserData);

Assert.Equal(-1, constant.GetValue(null!));
Assert.Equal(-1, _engine.Evaluate(preparedScript));
constant.GetValue(null!).AsNumber().Should().Be(-1);
new Engine().Evaluate(preparedScript).AsNumber().Should().Be(-1);
}

[Fact]
Expand All @@ -58,20 +58,37 @@ public void ScriptPreparationOptimizesConstantReturn()
var returnStatement = Assert.IsType<ConstantStatement>(statement.UserData);

var builtStatement = JintStatement.Build(statement);
Assert.Same(returnStatement, builtStatement);
returnStatement.Should().BeSameAs(builtStatement);

var result = builtStatement.Execute(new EvaluationContext(_engine)).Value;
Assert.Equal(JsBoolean.False, result);
var result = builtStatement.Execute(new EvaluationContext( new Engine())).Value;
result.Should().Be(JsBoolean.False);
}

[Fact]
public void CompiledRegexShouldProduceSameResultAsNonCompiled()
{
const string Script = """JSON.stringify(/(.*?)a(?!(a+)b\2c)\2(.*)/.exec("baaabaac"))""";

var nonCompiled = _engine.Evaluate(Script);
var compiled = _engine.Evaluate(Engine.PrepareScript(Script));
var engine = new Engine();
var nonCompiledResult = engine.Evaluate(Script);
var compiledResult = engine.Evaluate(Engine.PrepareScript(Script));

Assert.Equal(nonCompiled, compiled);
nonCompiledResult.Should().Be(compiledResult);
}

[Fact]
public void PrepareScriptShouldNotLeakAcornimaException()
{
var ex = Assert.Throws<ScriptPreparationException>(() => Engine.PrepareScript("class A { } A().#nonexistent = 1;"));
ex.Message.Should().Be("Could not prepare script: Private field '#nonexistent' must be declared in an enclosing class (1:17)");
ex.InnerException.Should().BeOfType<SyntaxErrorException>();
}

[Fact]
public void PrepareModuleShouldNotLeakAcornimaException()
{
var ex = Assert.Throws<ScriptPreparationException>(() => Engine.PrepareModule("class A { } A().#nonexistent = 1;"));
ex.Message.Should().Be("Could not prepare script: Private field '#nonexistent' must be declared in an enclosing class (1:17)");
ex.InnerException.Should().BeOfType<SyntaxErrorException>();
}
}
32 changes: 24 additions & 8 deletions Jint/Engine.Ast.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Jint.Native;
using Jint.Runtime;
using Jint.Runtime.Interpreter;
Expand All @@ -20,10 +19,20 @@ public partial class Engine
public static Prepared<Script> PrepareScript(string code, string? source = null, bool strict = false, ScriptPreparationOptions? options = null)
{
options ??= ScriptPreparationOptions.Default;

var astAnalyzer = new AstAnalyzer(options);
var parserOptions = options.GetParserOptions();
var preparedScript = new Parser(parserOptions with { OnNode = astAnalyzer.NodeVisitor }).ParseScript(code, source, strict);
return new Prepared<Script>(preparedScript, parserOptions);
var parser = new Parser(parserOptions with { OnNode = astAnalyzer.NodeVisitor });

try
{
var preparedScript = parser.ParseScript(code, source, strict);
return new Prepared<Script>(preparedScript, parserOptions);
}
catch (Exception e)
{
throw new ScriptPreparationException("Could not prepare script: " + e.Message, e);
}
}

/// <summary>
Expand All @@ -35,19 +44,26 @@ public static Prepared<Script> PrepareScript(string code, string? source = null,
public static Prepared<Module> PrepareModule(string code, string? source = null, ModulePreparationOptions? options = null)
{
options ??= ModulePreparationOptions.Default;

var astAnalyzer = new AstAnalyzer(options);
var parserOptions = options.GetParserOptions();
var preparedModule = new Parser(parserOptions with { OnNode = astAnalyzer.NodeVisitor }).ParseModule(code, source);
return new Prepared<Module>(preparedModule, parserOptions);
var parser = new Parser(parserOptions with { OnNode = astAnalyzer.NodeVisitor });

try
{
var preparedModule = parser.ParseModule(code, source);
return new Prepared<Module>(preparedModule, parserOptions);
}
catch (Exception e)
{
throw new ScriptPreparationException("Could not prepare script: " + e.Message, e);
}
}

private sealed class AstAnalyzer
{
private static readonly bool _canCompileNegativeLookaroundAssertions = typeof(Regex).Assembly.GetName().Version?.Major is not (null or 7 or 8);

private readonly IPreparationOptions<IParsingOptions> _preparationOptions;
private readonly Dictionary<string, Environment.BindingName> _bindingNames = new(StringComparer.Ordinal);
private readonly Dictionary<string, Regex> _regexes = new(StringComparer.Ordinal);

public AstAnalyzer(IPreparationOptions<IParsingOptions> preparationOptions)
{
Expand Down
15 changes: 15 additions & 0 deletions Jint/JintException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Jint;

/// <summary>
/// Base class for exceptions thrown by Jint.
/// </summary>
public abstract class JintException : Exception
{
internal JintException(string? message) : base(message)
{
}

internal JintException(string? message, Exception? innerException) : base(message, innerException)
{
}
}
20 changes: 0 additions & 20 deletions Jint/Runtime/JintException.cs

This file was deleted.

8 changes: 8 additions & 0 deletions Jint/ScriptPreparationException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Jint;

public sealed class ScriptPreparationException : JintException
{
public ScriptPreparationException(string? message, Exception? innerException) : base(message, innerException)
{
}
}