Skip to content

Commit

Permalink
Implement top-level await in modules feature (#361)
Browse files Browse the repository at this point in the history
* Enable top level await in modules (and expressions)

* Enable test262 tests
  • Loading branch information
adams85 authored Dec 29, 2022
1 parent 7696695 commit 0828a8b
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 34 deletions.
11 changes: 9 additions & 2 deletions src/Esprima/JavascriptParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ public Module ParseModule(string code, string? source = null)
try
{
_context.Strict = true;
_context.IsAsync = true;
_context.IsModule = true;
_scanner._isModule = true;

Expand Down Expand Up @@ -675,7 +676,7 @@ private protected virtual Expression ParsePrimaryExpression()
switch (_lookahead.Type)
{
case TokenType.Identifier:
if ((_context.IsModule || _context.IsAsync) && "await".Equals(_lookahead.Value))
if (_context.IsAsync && "await".Equals(_lookahead.Value))
{
TolerateUnexpectedToken(_lookahead);
}
Expand Down Expand Up @@ -2058,6 +2059,11 @@ private Expression ParseUnaryExpression()
}
else if (_context.IsAsync && MatchContextualKeyword("await"))
{
if (_lookahead.End - _lookahead.Start != "await".Length)
{
TolerateUnexpectedToken(_lookahead, Messages.InvalidEscapedReservedWord);
}

expr = ParseAwaitExpression();
}
else
Expand Down Expand Up @@ -2668,6 +2674,7 @@ public Expression ParseExpression(string code)
Reset(code, source: null);
try
{
_context.IsAsync = true;
return FinalizeRoot(ParseExpression());
}
finally
Expand Down Expand Up @@ -3079,7 +3086,7 @@ private Identifier ParseVariableIdentifier(VariableDeclarationKind? kind = null,
}
}
}
else if ((_context.IsModule || _context.IsAsync) && !allowAwaitKeyword && token.Type == TokenType.Identifier && (string?) token.Value == "await")
else if (_context.IsAsync && !allowAwaitKeyword && token.Type == TokenType.Identifier && (string?) token.Value == "await")
{
TolerateUnexpectedToken(token);
}
Expand Down
3 changes: 1 addition & 2 deletions test/Esprima.Tests.Test262/Test262Harness.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
"Namespace": "Esprima.Tests.Test262",
"Parallel": true,
"ExcludedFeatures": [
"regexp-unicode-property-escapes",
"top-level-await"
"regexp-unicode-property-escapes"
],
"ExcludedFlags": [],
"ExcludedDirectories": [],
Expand Down
58 changes: 30 additions & 28 deletions test/Esprima.Tests/Fixtures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,14 @@ public void ExecuteTestCase(string fixture)
metadata = FixtureMetadata.Default;
}

if (metadata.Skip)
{
return;
}

var parserOptions = parserOptionsFactory(false, !metadata.IgnoresRegex);

var conversionOptions = metadata.CreateConversionOptions(conversionDefaultOptions);

#pragma warning disable 162
if (File.Exists(moduleFilePath))
{
sourceType = SourceType.Module;
Expand Down Expand Up @@ -182,7 +185,6 @@ public void ExecuteTestCase(string fixture)
if (!CompareTrees(actual, expected, metadata))
File.WriteAllText(failureFilePath, actual);
}
#pragma warning restore 162
}
else
{
Expand Down Expand Up @@ -236,12 +238,15 @@ internal static string GetFixturesPath()

private sealed class FixtureMetadata
{
public static readonly FixtureMetadata Default = new FixtureMetadata(
testCompatibilityMode: AstToJsonTestCompatibilityMode.None,
includesLocation: true,
includesRange: true,
includesLocationSource: false,
ignoresRegex: false);
public static readonly FixtureMetadata Default = new()
{
TestCompatibilityMode = AstToJsonTestCompatibilityMode.None,
IncludesLocation = true,
IncludesRange = true,
IncludesLocationSource = false,
IgnoresRegex = false,
Skip = false,
};

private sealed class Group
{
Expand Down Expand Up @@ -276,28 +281,25 @@ public static Dictionary<string, FixtureMetadata> ReadMetadata()

private static FixtureMetadata CreateFrom(HashSet<string> flags)
{
return new FixtureMetadata(
testCompatibilityMode: flags.Contains("EsprimaOrgFixture") ? AstToJsonTestCompatibilityMode.EsprimaOrg : AstToJsonTestCompatibilityMode.None,
includesLocation: flags.Contains("IncludesLocation"),
includesRange: flags.Contains("IncludesRange"),
includesLocationSource: flags.Contains("IncludesLocationSource"),
ignoresRegex: flags.Contains("IgnoresRegex"));
return new FixtureMetadata
{
TestCompatibilityMode = flags.Contains("EsprimaOrgFixture") ? AstToJsonTestCompatibilityMode.EsprimaOrg : AstToJsonTestCompatibilityMode.None,
IncludesLocation = flags.Contains("IncludesLocation"),
IncludesRange = flags.Contains("IncludesRange"),
IncludesLocationSource = flags.Contains("IncludesLocationSource"),
IgnoresRegex = flags.Contains("IgnoresRegex"),
Skip = flags.Contains("Skip"),
};
}

private FixtureMetadata(AstToJsonTestCompatibilityMode testCompatibilityMode, bool includesLocation, bool includesRange, bool includesLocationSource, bool ignoresRegex)
{
TestCompatibilityMode = testCompatibilityMode;
IncludesLocation = includesLocation;
IncludesRange = includesRange;
IncludesLocationSource = includesLocationSource;
IgnoresRegex = ignoresRegex;
}
private FixtureMetadata() { }

public AstToJsonTestCompatibilityMode TestCompatibilityMode { get; }
public bool IncludesLocation { get; }
public bool IncludesRange { get; }
public bool IncludesLocationSource { get; }
public bool IgnoresRegex { get; }
public AstToJsonTestCompatibilityMode TestCompatibilityMode { get; init; }
public bool IncludesLocation { get; init; }
public bool IncludesRange { get; init; }
public bool IncludesLocationSource { get; init; }
public bool IgnoresRegex { get; init; }
public bool Skip { get; init; }

public AstToJsonOptions CreateConversionOptions(AstToJsonOptions defaultOptions) => defaultOptions with
{
Expand Down
9 changes: 7 additions & 2 deletions test/Esprima.Tests/Fixtures/fixtures-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,6 @@
"ES6/identifier/estimated.js",
"ES6/identifier/ethiopic_digits.js",
"ES6/identifier/invalid_escaped_surrogate_pairs.js",
"ES6/identifier/invalid_expression_await.module.js",
"ES6/identifier/invalid_function_await.module.js",
"ES6/identifier/invalid_id_smp.js",
"ES6/identifier/invalid_lone_surrogate.source.js",
Expand All @@ -629,7 +628,6 @@
"ES6/identifier/math_dal_part.js",
"ES6/identifier/math_kaf_lam.js",
"ES6/identifier/math_zain_start.js",
"ES6/identifier/module_await.js",
"ES6/identifier/valid_await.js",
"ES6/identifier/weierstrass.js",
"ES6/identifier/weierstrass_weierstrass.js",
Expand Down Expand Up @@ -1710,5 +1708,12 @@
"expression/primary/literal/numeric/migrated_0003.js",
"expression/primary/literal/regular-expression/migrated_0008.js"
]
},
{
"flags": [ "EsprimaOrgFixture", "Skip" ],
"files": [
"ES6/identifier/invalid_expression_await.module.js",
"ES6/identifier/module_await.js"
]
}
]
30 changes: 30 additions & 0 deletions test/Esprima.Tests/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -616,4 +616,34 @@ class X {

Assert.Equal(expected, json);
}

[Theory]
[InlineData("script", true)]
[InlineData("module", false)]
[InlineData("expression", false)]
public void ShouldParseTopLevelAwait(string sourceType, bool shouldThrow)
{
const string code = "await import('x')";

var parser = new JavaScriptParser();
Func<JavaScriptParser, Node> parseAction = sourceType switch
{
"script" => parser => parser.ParseScript(code),
"module" => parser => parser.ParseModule(code),
"expression" => parser => parser.ParseExpression(code),
_ => throw new InvalidOperationException()
};

if (!shouldThrow)
{
var node = parseAction(parser);
var awaitExpression = node.DescendantNodesAndSelf().OfType<AwaitExpression>().FirstOrDefault();
Assert.NotNull(awaitExpression);
Assert.IsType<Import>(awaitExpression.Argument);
}
else
{
Assert.Throws<ParserException>(() => parseAction(parser));
}
}
}

0 comments on commit 0828a8b

Please sign in to comment.