Skip to content
Open
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
23 changes: 19 additions & 4 deletions src/Starling.Js/Parse/JsParser.Modules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,29 @@ private Statement ParseProgramStatement()
var next = _lex.Peek().Kind;
if (next is JsTokenKind.LParen or JsTokenKind.Dot)
return ParseStatement();
// §16.2.2 — a static ImportDeclaration is a module-only production.
// In a script (the default goal) a top-level `import …` is an early
// SyntaxError (sec-scripts: a ScriptBody is just a StatementList,
// which contains no module items).
if (!_module)
throw new JsParseException(
"import declarations may only appear at the top level of a module",
_current.Start);
return ParseImportDeclaration();
}

return _current.Kind switch
if (_current.Kind == JsTokenKind.Export)
{
JsTokenKind.Export => ParseExportDeclaration(),
_ => ParseStatement(),
};
// §16.2.3 — ExportDeclaration is module-only; a top-level `export …`
// in a script is an early SyntaxError for the same reason.
if (!_module)
throw new JsParseException(
"export declarations may only appear at the top level of a module",
_current.Start);
return ParseExportDeclaration();
}

return ParseStatement();
}

/// <summary>wp:M3-03c — parse the expression-context forms of <c>import</c>:
Expand Down
57 changes: 50 additions & 7 deletions tests/Starling.Js.Tests/Parse/JsParserModuleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public void Import_named_empty_list()

[TestMethod]
public void Import_allows_asi_after_source()
=> ParseProgram("import x from 'm'\nimport y from 'n'").Body.Should().HaveCount(2);
=> ParseModule("import x from 'm'\nimport y from 'n'").Body.Should().HaveCount(2);

[TestMethod]
public void Import_missing_from_throws()
Expand Down Expand Up @@ -146,7 +146,10 @@ public void Export_class_declaration()
[TestMethod]
public void Export_named_list()
{
var export = ParseSingle<ExportNamedDeclaration>("export { a, b as c, default as d };");
// The local names must be declared in the module, else the export is an
// early error ("export X is not defined in module").
var body = ParseModule("let a, b, d; export { a, b as c, d };").Body;
var export = body[^1].Should().BeOfType<ExportNamedDeclaration>().Subject;
export.Source.Should().BeNull();
export.Specifiers.Should().HaveCount(3);
((Identifier)export.Specifiers[1].Exported).Name.Should().Be("c");
Expand All @@ -155,7 +158,8 @@ public void Export_named_list()
[TestMethod]
public void Export_named_list_with_string_export_name()
{
var spec = ParseSingle<ExportNamedDeclaration>("export { internal as 'public-name' };")
var body = ParseModule("let internal; export { internal as 'public-name' };").Body;
var spec = body[^1].Should().BeOfType<ExportNamedDeclaration>().Subject
.Specifiers.Should().ContainSingle().Subject;
((Identifier)spec.Local).Name.Should().Be("internal");
((StringLiteral)spec.Exported).Value.Should().Be("public-name");
Expand Down Expand Up @@ -236,23 +240,62 @@ public void Export_declaration_is_top_level_only()
[TestMethod]
public void Program_can_mix_imports_exports_and_statements()
{
var program = ParseProgram("import x from 'm'; export { x }; x();");
var program = ParseModule("import x from 'm'; export { x }; x();");
program.Body[0].Should().BeOfType<ImportDeclaration>();
program.Body[1].Should().BeOfType<ExportNamedDeclaration>();
program.Body[2].Should().BeOfType<ExpressionStatement>();
}

// Static import/export are module-only productions (§16.2). In a script (the
// default goal) a top-level import/export is an early SyntaxError.
[TestMethod]
public void Script_import_declaration_throws()
=> FailsAsScript("import x from 'mod';");

[TestMethod]
public void Script_side_effect_import_throws()
=> FailsAsScript("import 'polyfill';");

[TestMethod]
public void Script_export_named_throws()
=> FailsAsScript("var x; export { x };");

[TestMethod]
public void Script_export_default_throws()
=> FailsAsScript("export default 1;");

[TestMethod]
public void Script_export_local_declaration_throws()
=> FailsAsScript("export const a = 1;");

// import(...) is an expression form, not a module item, so it parses in a script.
[TestMethod]
public void Script_dynamic_import_call_parses()
{
var stmt = ParseScript("import('mod');").Body.Should().ContainSingle().Subject;
stmt.Should().BeOfType<ExpressionStatement>()
.Which.Expression.Should().BeOfType<ImportCallExpression>();
}

private static T ParseSingle<T>(string source) where T : Statement
{
var statement = ParseProgram(source).Body.Should().ContainSingle().Subject;
var statement = ParseModule(source).Body.Should().ContainSingle().Subject;
return statement.Should().BeOfType<T>().Subject;
}

private static Program ParseProgram(string source) => new JsParser(source).ParseProgram();
private static Program ParseModule(string source) => new JsParser(source).ParseModule();

private static Program ParseScript(string source) => new JsParser(source).ParseProgram();

private static void Fails(string source)
{
var act = () => ParseProgram(source);
var act = () => ParseModule(source);
act.Should().Throw<JsParseException>();
}

private static void FailsAsScript(string source)
{
var act = () => ParseScript(source);
act.Should().Throw<JsParseException>();
}
}
Loading