Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
51 changes: 51 additions & 0 deletions Jint.Tests.Test262/Language/ModuleTestHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.IO;
using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime;
using Jint.Runtime.Interop;

namespace Jint.Tests.Test262.Language
{
// Hacky way to get objects from assert.js and sta.js into the module context
internal sealed class ModuleTestHost : Host
{
private readonly static Dictionary<string, JsValue> _staticValues = new();

static ModuleTestHost()
{
var assemblyPath = new Uri(typeof(ModuleTestHost).GetTypeInfo().Assembly.Location).LocalPath;
var assemblyDirectory = new FileInfo(assemblyPath).Directory;

var basePath = assemblyDirectory.Parent.Parent.Parent.FullName;

var engine = new Engine();
var assertSource = File.ReadAllText(Path.Combine(basePath, "harness", "assert.js"));
var staSource = File.ReadAllText(Path.Combine(basePath, "harness", "sta.js"));

engine.Execute(assertSource);
engine.Execute(staSource);

_staticValues["assert"] = engine.GetValue("assert");
_staticValues["Test262Error"] = engine.GetValue("Test262Error");
_staticValues["$ERROR"] = engine.GetValue("$ERROR");
_staticValues["$DONOTEVALUATE"] = engine.GetValue("$DONOTEVALUATE");

_staticValues["print"] = new ClrFunctionInstance(engine, "print", (thisObj, args) => TypeConverter.ToString(args.At(0)));
}

protected override ObjectInstance CreateGlobalObject(Realm realm)
{
var globalObj = base.CreateGlobalObject(realm);

foreach (var key in _staticValues.Keys)
{
globalObj.FastAddProperty(key, _staticValues[key], true, true, true);
}

return globalObj;
}
}
}
73 changes: 56 additions & 17 deletions Jint.Tests.Test262/Language/ModuleTests.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,70 @@
using Jint.Runtime;
using System;
using Xunit;
using Xunit.Sdk;

namespace Jint.Tests.Test262.Language
namespace Jint.Tests.Test262.Language;

public class ModuleTests : Test262Test
{
public class ModuleTests : Test262Test
[Theory(DisplayName = "language\\module-code")]
[MemberData(nameof(SourceFiles), "language\\module-code", false)]
[MemberData(nameof(SourceFiles), "language\\module-code", true, Skip = "Skipped")]
protected void ModuleCode(SourceFile sourceFile)
{
RunModuleTest(sourceFile);
}

[Theory(DisplayName = "language\\export")]
[MemberData(nameof(SourceFiles), "language\\export", false)]
[MemberData(nameof(SourceFiles), "language\\export", true, Skip = "Skipped")]
protected void Export(SourceFile sourceFile)
{
RunModuleTest(sourceFile);
}

[Theory(DisplayName = "language\\import")]
[MemberData(nameof(SourceFiles), "language\\import", false)]
[MemberData(nameof(SourceFiles), "language\\import", true, Skip = "Skipped")]
protected void Import(SourceFile sourceFile)
{
RunModuleTest(sourceFile);
}

private static void RunModuleTest(SourceFile sourceFile)
{
[Theory(DisplayName = "language\\module-code", Skip = "TODO")]
[MemberData(nameof(SourceFiles), "language\\module-code", false)]
[MemberData(nameof(SourceFiles), "language\\module-code", true, Skip = "Skipped")]
protected void ModuleCode(SourceFile sourceFile)
if (sourceFile.Skip)
{
RunTestInternal(sourceFile);
return;
}

[Theory(DisplayName = "language\\export")]
[MemberData(nameof(SourceFiles), "language\\export", false)]
[MemberData(nameof(SourceFiles), "language\\export", true, Skip = "Skipped")]
protected void Export(SourceFile sourceFile)
var code = sourceFile.Code;

var options = new Options();
options.Host.Factory = _ => new ModuleTestHost();
options.Modules.Enabled = true;

var engine = new Engine(options);

var negative = code.IndexOf("negative:", StringComparison.OrdinalIgnoreCase) != -1;
string lastError = null;

try
{
engine.LoadModule(sourceFile.FullPath);
}
catch (JavaScriptException ex)
{
lastError = ex.ToString();
}
catch (Exception ex)
{
RunTestInternal(sourceFile);
lastError = ex.ToString();
}

[Theory(DisplayName = "language\\import", Skip = "TODO")]
[MemberData(nameof(SourceFiles), "language\\import", false)]
[MemberData(nameof(SourceFiles), "language\\import", true, Skip = "Skipped")]
protected void Import(SourceFile sourceFile)
if (!negative && !string.IsNullOrWhiteSpace(lastError))
{
RunTestInternal(sourceFile);
throw new XunitException(lastError);
}
}
}
19 changes: 19 additions & 0 deletions Jint.Tests.Test262/Test262Test.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,13 @@ public static IEnumerable<object[]> SourceFiles(string pathPrefix, bool skipped)

foreach (var file in files)
{
if (file.IndexOf("_FIXTURE", StringComparison.OrdinalIgnoreCase) != -1)
{
// Files bearing a name which includes the sequence _FIXTURE MUST NOT be interpreted
// as standalone tests; they are intended to be referenced by test files.
continue;
}

var name = file.Substring(fixturesPath.Length + 1).Replace("\\", "/");
bool skip = _skipReasons.TryGetValue(name, out var reason);

Expand Down Expand Up @@ -288,6 +295,18 @@ public static IEnumerable<object[]> SourceFiles(string pathPrefix, bool skipped)
skip = true;
reason = "resizable-arraybuffer not implemented";
break;
case "json-modules":
skip = true;
reason = "json-modules not implemented";
break;
case "top-level-await":
skip = true;
reason = "top-level-await not implemented";
break;
case "import-assertions":
skip = true;
reason = "import-assertions not implemented";
break;
}
}
}
Expand Down
57 changes: 57 additions & 0 deletions Jint/Engine.Modules.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Collections.Generic;
using Esprima;
using Esprima.Ast;
using Jint.Runtime;
using Jint.Runtime.Modules;

namespace Jint
{
public partial class Engine
{
public IModuleLoader ModuleLoader { get; internal set; }

private readonly Dictionary<ModuleCacheKey, JsModule> _modules = new();

public JsModule LoadModule(string specifier) => LoadModule(null, specifier);

internal JsModule LoadModule(string referencingModuleLocation, string specifier)
{
var key = new ModuleCacheKey(referencingModuleLocation ?? string.Empty, specifier);

if (_modules.TryGetValue(key, out var module))
{
return module;
}

if (!ModuleLoader.TryLoadModule(specifier, referencingModuleLocation, out var moduleSourceCode, out var moduleLocation))
Copy link
Owner

Choose a reason for hiding this comment

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

What are your thoughts about returning the AST from the module loader so it could cache it. Right now it can only cache the source. Would it be beneficial, like a loader could be shared across engines, and even provide some sort of thundering-herd protection? Note that it could already do that and still return a string. Could even be a custom loader wrapper.

Today we can cache parsed programs, and create an engine from that, but returning a string means parsed modules can't be cached, so each execution will have to parse the same module again.

{
ExceptionHelper.ThrowSyntaxError(Realm, "Error while loading module: module with specifier '" + specifier + "' could not be located");
}

Module moduleSource;
try
{
var parserOptions = new ParserOptions(moduleLocation)
{
AdaptRegexp = true,
Tolerant = true
};

moduleSource = new JavaScriptParser(moduleSourceCode, parserOptions).ParseModule();
}
catch (ParserException ex)
{
ExceptionHelper.ThrowSyntaxError(Realm, "Error while loading module: error in module '" + specifier + "': " + ex.Error?.ToString() ?? ex.Message);
moduleSource = null;
}

module = new JsModule(this, _host.CreateRealm(), moduleSource, moduleLocation, false);

_modules[key] = module;

return module;
}

internal readonly record struct ModuleCacheKey(string ReferencingModuleLocation, string Specifier);
}
}
127 changes: 127 additions & 0 deletions Jint/EsprimaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Jint.Runtime.Environments;
using Jint.Runtime.Interpreter;
using Jint.Runtime.Interpreter.Expressions;
using Jint.Runtime.Modules;

namespace Jint
{
Expand Down Expand Up @@ -128,6 +129,7 @@ internal static string LiteralKeyToString(Literal literal)
{
return TypeConverter.ToString(d);
}

return literal.Value as string ?? Convert.ToString(literal.Value, provider: null);
}

Expand Down Expand Up @@ -204,6 +206,7 @@ internal static void GetBoundNames(this Node? parameter, List<string> target)
parameter = assignmentPattern.Left;
continue;
}

break;
}
}
Expand Down Expand Up @@ -256,6 +259,130 @@ internal static Record DefineMethod(this ClassProperty m, ObjectInstance obj, Ob
return new Record(property, closure);
}

internal static void GetImportEntries(this ImportDeclaration import, List<ImportEntry> importEntries, HashSet<string> requestedModules)
{
var source = import.Source.StringValue;
var specifiers = import.Specifiers;
requestedModules.Add(source!);

foreach (var specifier in specifiers)
{
switch (specifier)
{
case ImportNamespaceSpecifier namespaceSpecifier:
importEntries.Add(new ImportEntry(source, "*", namespaceSpecifier.Local.GetModuleKey()));
break;
case ImportSpecifier importSpecifier:
importEntries.Add(new ImportEntry(source, importSpecifier.Imported.GetModuleKey(), importSpecifier.Local.GetModuleKey()));
break;
case ImportDefaultSpecifier defaultSpecifier:
importEntries.Add(new ImportEntry(source, "default", defaultSpecifier.Local.GetModuleKey()));
break;
}
}
}

internal static void GetExportEntries(this ExportDeclaration export, List<ExportEntry> exportEntries, HashSet<string> requestedModules)
{
switch (export)
{
case ExportDefaultDeclaration defaultDeclaration:
GetExportEntries(true, defaultDeclaration.Declaration, exportEntries);
break;
case ExportAllDeclaration allDeclaration:
//Note: there is a pending PR for Esprima to support exporting an imported modules content as a namespace i.e. 'export * as ns from "mod"'
Copy link
Owner

Choose a reason for hiding this comment

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

Still?

requestedModules.Add(allDeclaration.Source.StringValue!);
exportEntries.Add(new(null, allDeclaration.Source.StringValue, "*", null));
break;
case ExportNamedDeclaration namedDeclaration:
var specifiers = namedDeclaration.Specifiers;
if (specifiers.Count == 0)
{
GetExportEntries(false, namedDeclaration.Declaration!, exportEntries, namedDeclaration.Source?.StringValue);

if (namedDeclaration.Source is not null)
{
requestedModules.Add(namedDeclaration.Source.StringValue!);
}
}
else
{
foreach (var specifier in specifiers)
{
exportEntries.Add(new(specifier.Local.GetModuleKey(), namedDeclaration.Source?.StringValue, specifier.Exported.GetModuleKey(), null));
}
}

break;
}
}

private static void GetExportEntries(bool defaultExport, StatementListItem declaration, List<ExportEntry> exportEntries, string? moduleRequest = null)
{
var names = GetExportNames(declaration);

if (names.Count == 0)
{
if (defaultExport)
{
exportEntries.Add(new("default", null, null, "*default*"));
}
}
else
{
for (var i = 0; i < names.Count; i++)
{
var name = names[i];
var exportName = defaultExport ? "default" : name;
exportEntries.Add(new(exportName, moduleRequest, null, name));
}
}
}

private static List<string> GetExportNames(StatementListItem declaration)
{
var result = new List<string>();

switch (declaration)
{
case FunctionDeclaration functionDeclaration:
var funcName = functionDeclaration.Id?.Name;
if (funcName is not null)
{
result.Add(funcName);
}

break;
case ClassDeclaration classDeclaration:
var className = classDeclaration.Id?.Name;
if (className is not null)
{
result.Add(className);
}

break;
case VariableDeclaration variableDeclaration:
var declarators = variableDeclaration.Declarations;
foreach (var declarator in declarators)
{
var varName = declarator.Id.As<Identifier>()?.Name;
if (varName is not null)
{
result.Add(varName);
}
}

break;
}

return result;
}

private static string? GetModuleKey(this Expression expression)
{
return (expression as Identifier)?.Name ?? (expression as Literal)?.StringValue;
}

internal readonly record struct Record(JsValue Key, ScriptFunctionInstance Closure);
}
}
Loading