From 88b0b81915949d0c5f6e41c57b38ae8c43203de3 Mon Sep 17 00:00:00 2001 From: Cody Mullins <1738479+codymullins@users.noreply.github.com> Date: Tue, 16 Jun 2026 21:53:51 -0400 Subject: [PATCH 1/2] fix(js): reject top-level import/export in scripts as a SyntaxError Static ImportDeclaration and ExportDeclaration are module-only productions (spec sec-modules). In a script, which is the default goal, the parser accepted them and the compiler later threw an internal NotSupportedException, so a negative parse test saw the wrong failure (got NotSupportedException instead of a parse error). Guard the static import/export dispatch in ParseProgramStatement on the module goal. In a script a top-level import/export now throws a JsParseException, which the eval path surfaces as a JS SyntaxError. The import(...) and import.meta expression forms are unaffected (they route to ParseStatement before the guard). Test262 language/global-code 53.3% -> 58.7%, language/eval-code 86.6% -> 87.4% (+8 scenarios across both, the import/export parse tests). No regression: the new parse error fires on zero positive tests. The full-language headline is flat only because an unrelated batch of flaky async/dynamic-import tests in expressions/statements wobbled by the same magnitude this run. --- src/Starling.Js/Parse/JsParser.Modules.cs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Starling.Js/Parse/JsParser.Modules.cs b/src/Starling.Js/Parse/JsParser.Modules.cs index 17d62814..2c348347 100644 --- a/src/Starling.Js/Parse/JsParser.Modules.cs +++ b/src/Starling.Js/Parse/JsParser.Modules.cs @@ -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(); } /// wp:M3-03c — parse the expression-context forms of import: From cd5990a042c2ae38ac64d683fc15133db069b45c Mon Sep 17 00:00:00 2001 From: Cody Mullins <1738479+codymullins@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:42:52 -0400 Subject: [PATCH 2/2] test(js): cover script-goal import/export rejection The module-syntax parser tests parsed import/export through ParseProgram (the script goal), which now rejects them. Route those tests through the module goal where the productions are valid, and add tests that a top-level import/export in a script throws JsParseException while import(...) still parses. Two export-list tests declare their local bindings to satisfy the module-goal early-error pass. --- .../Parse/JsParserModuleTests.cs | 57 ++++++++++++++++--- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/tests/Starling.Js.Tests/Parse/JsParserModuleTests.cs b/tests/Starling.Js.Tests/Parse/JsParserModuleTests.cs index 00a5e724..a48ccaf6 100644 --- a/tests/Starling.Js.Tests/Parse/JsParserModuleTests.cs +++ b/tests/Starling.Js.Tests/Parse/JsParserModuleTests.cs @@ -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() @@ -146,7 +146,10 @@ public void Export_class_declaration() [TestMethod] public void Export_named_list() { - var export = ParseSingle("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().Subject; export.Source.Should().BeNull(); export.Specifiers.Should().HaveCount(3); ((Identifier)export.Specifiers[1].Exported).Name.Should().Be("c"); @@ -155,7 +158,8 @@ public void Export_named_list() [TestMethod] public void Export_named_list_with_string_export_name() { - var spec = ParseSingle("export { internal as 'public-name' };") + var body = ParseModule("let internal; export { internal as 'public-name' };").Body; + var spec = body[^1].Should().BeOfType().Subject .Specifiers.Should().ContainSingle().Subject; ((Identifier)spec.Local).Name.Should().Be("internal"); ((StringLiteral)spec.Exported).Value.Should().Be("public-name"); @@ -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(); program.Body[1].Should().BeOfType(); program.Body[2].Should().BeOfType(); } + // 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() + .Which.Expression.Should().BeOfType(); + } + private static T ParseSingle(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().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(); + } + + private static void FailsAsScript(string source) + { + var act = () => ParseScript(source); act.Should().Throw(); } }