diff --git a/SharpTS.Tests/SharedTests/AsyncGeneratorTests.cs b/SharpTS.Tests/SharedTests/AsyncGeneratorTests.cs index ad0ed02c..67a6ca7b 100644 --- a/SharpTS.Tests/SharedTests/AsyncGeneratorTests.cs +++ b/SharpTS.Tests/SharedTests/AsyncGeneratorTests.cs @@ -1,4 +1,5 @@ using SharpTS.Tests.Infrastructure; +using SharpTS.TypeSystem.Exceptions; using Xunit; namespace SharpTS.Tests.SharedTests; @@ -329,6 +330,41 @@ async function main() { Assert.Equal("first: 1\nfirst: 2\nsecond: 1\nsecond: 2\n", output); } + // #672: a top-level `for await...of` drives the async-iterator protocol, which requires an async + // context. SharpTS does not support top-level await, so — like a top-level `await` expression — + // it must be rejected by the type checker rather than silently degrading to a synchronous + // `for...of` (which then throws a misleading 'not iterable' runtime error in both modes). + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ForAwaitOf_TopLevel_RejectedByTypeChecker(ExecutionMode mode) + { + var source = """ + async function* g() { yield 1; yield 2; } + for await (const x of g()) console.log("top", x); + """; + + var ex = Assert.Throws(() => TestHarness.Run(source, mode)); + Assert.Contains("'await' is only valid inside an async function.", ex.Message); + } + + // #672: `for await...of` inside a non-async function is likewise an await outside an async + // context and must be rejected (TS conformance), not run as a synchronous `for...of`. + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ForAwaitOf_InNonAsyncFunction_RejectedByTypeChecker(ExecutionMode mode) + { + var source = """ + async function* g() { yield 1; yield 2; } + function notAsync() { + for await (const x of g()) console.log(x); + } + notAsync(); + """; + + var ex = Assert.Throws(() => TestHarness.Run(source, mode)); + Assert.Contains("'await' is only valid inside an async function.", ex.Message); + } + #endregion #region Async Generator .return() and .throw() Tests diff --git a/TypeSystem/TypeChecker.Statements.cs b/TypeSystem/TypeChecker.Statements.cs index c60c1e11..4642e014 100644 --- a/TypeSystem/TypeChecker.Statements.cs +++ b/TypeSystem/TypeChecker.Statements.cs @@ -825,6 +825,18 @@ internal VoidResult VisitFor(Stmt.For stmt) internal VoidResult VisitForOf(Stmt.ForOf stmt) { + // `for await...of` drives the async-iterator protocol, so — like an `await` expression or + // `await using` — it is only valid inside an async function. SharpTS does not support + // top-level await, so a top-level (or otherwise non-async-context) `for await` is rejected + // here rather than silently degrading to a synchronous `for...of`, which would then fail at + // runtime with a misleading 'not iterable' error (#672). Mirrors CheckAwait / CheckUsingDeclaration. + if (stmt.IsAsync && !_inAsyncFunction) + { + throw new TypeCheckException( + "'await' is only valid inside an async function.", + stmt.Variable.Line, tsCode: "TS1308"); + } + TypeInfo iterableType = CheckExpr(stmt.Iterable); // Now that generator yield-type inference draws from the `yield` / `yield*` operands (#548), the