diff --git a/Jint.Tests/Runtime/EngineTests.cs b/Jint.Tests/Runtime/EngineTests.cs index 31b6b19d7..bcdcbd041 100644 --- a/Jint.Tests/Runtime/EngineTests.cs +++ b/Jint.Tests/Runtime/EngineTests.cs @@ -2041,17 +2041,17 @@ public void RegExpPrototypeToString() RunTest("assert(RegExp.prototype.toString() === '/(?:)/');"); } - [Fact] - public void ShouldSetYearBefore1970() - { - new Engine(options => options.LocalTimeZone(TimeZoneInfo.Utc)) - .SetValue("equal", new Action(Assert.Equal)) - .Execute(@" - var d = new Date('1969-01-01T08:17:00Z'); - d.setYear(2015); - equal('2015-01-01T08:17:00.000Z', d.toISOString()); - "); - } + [Fact] + public void ShouldSetYearBefore1970() + { + new Engine(options => options.LocalTimeZone(TimeZoneInfo.Utc)) + .SetValue("equal", new Action(Assert.Equal)) + .Execute(@" + var d = new Date('1969-01-01T08:17:00Z'); + d.setYear(2015); + equal('2015-01-01T08:17:00.000Z', d.toISOString()); + "); + } [Fact] public void ShouldUseReplaceMarkers() @@ -2873,6 +2873,30 @@ public void RecursiveCallStack() Assert.Equal(1389, result); } + [Fact] + public void NestedEvaluateDuringContinuationDrainDoesNotClobberOuterResult() + { + // Regression test for https://github.com/sebastienros/jint/issues/2492 + // A queued microtask, drained while a nested Evaluate() is running, used to overwrite + // the engine-level completion value that the nested Evaluate() was about to return. + var engine = new Engine(); + engine.SetValue("evalB", new Func(() => engine.Evaluate("'B'"))); + engine.SetValue("evalC", new Func(() => engine.Evaluate("'C'"))); + + // Promise.resolve().then(evalC) queues a microtask. evalB()'s nested Evaluate drains it + // (RunAvailableContinuations), running evalC() -> a deeper Evaluate that completes with 'C'. + // Pre-fix, 'C' leaked into the shared field that evalB()'s Evaluate read back, so the + // outer script observed 'C' instead of 'B'. + var result = engine.Evaluate(@" + var captured; + Promise.resolve().then(() => { evalC(); }); + captured = evalB(); + captured; + "); + + Assert.Equal("B", result.AsString()); + } + [Fact] public void MemberExpressionInObjectProperty() { @@ -2899,43 +2923,43 @@ public void MemberExpressionInObjectProperty() } [Fact] - public void TypeofShouldEvaluateOnce() - { - var engine = new Engine(); - var result = engine.Evaluate(@" - let res = 0; + public void TypeofShouldEvaluateOnce() + { + var engine = new Engine(); + var result = engine.Evaluate(@" + let res = 0; const fn = () => res++; typeof fn(); res; ") .AsNumber(); - - Assert.Equal(1, result); - } - - [Fact] - public void MemberExpressionOnAssignmentShouldEvaluateRightHandSideOnce() - { - var engine = new Engine(); - - engine.Execute(@" - var callCount = 0; - function produce() { callCount++; return 'abc'; } - var x; - var len = (x = produce()).length; - "); - - Assert.Equal(1, engine.Evaluate("callCount").AsNumber()); - Assert.Equal(3, engine.Evaluate("len").AsNumber()); - Assert.Equal("abc", engine.Evaluate("x").AsString()); - } - - [Fact] - public void ClassDeclarationHoisting() - { - var ex = Assert.Throws(() => _engine.Evaluate("typeof MyClass; class MyClass {}")); - Assert.Equal("Cannot access 'MyClass' before initialization", ex.Message); - } + + Assert.Equal(1, result); + } + + [Fact] + public void MemberExpressionOnAssignmentShouldEvaluateRightHandSideOnce() + { + var engine = new Engine(); + + engine.Execute(@" + var callCount = 0; + function produce() { callCount++; return 'abc'; } + var x; + var len = (x = produce()).length; + "); + + Assert.Equal(1, engine.Evaluate("callCount").AsNumber()); + Assert.Equal(3, engine.Evaluate("len").AsNumber()); + Assert.Equal("abc", engine.Evaluate("x").AsString()); + } + + [Fact] + public void ClassDeclarationHoisting() + { + var ex = Assert.Throws(() => _engine.Evaluate("typeof MyClass; class MyClass {}")); + Assert.Equal("Cannot access 'MyClass' before initialization", ex.Message); + } [Fact] public void ShouldObeyScriptLevelStrictModeInFunctions() diff --git a/Jint.Tests/Runtime/ModuleTests.cs b/Jint.Tests/Runtime/ModuleTests.cs index da9ba53b3..74cb8fc5c 100644 --- a/Jint.Tests/Runtime/ModuleTests.cs +++ b/Jint.Tests/Runtime/ModuleTests.cs @@ -121,6 +121,60 @@ public void ShouldImportDynamically() Assert.True(received); } + [Fact] + public void NestedEvaluateFromModuleExecutionDoesNotClobberCompletionValue() + { + // Regression test for https://github.com/sebastienros/jint/issues/2492 + // A CLR function (importLibrary) calls back into engine.Evaluate(). While that nested + // Evaluate() drains the event loop, an awaited dynamic import runs module-two, which also + // calls importLibrary() -> a deeper Evaluate(). Pre-fix, the deeper completion value + // leaked into the field the first importLibrary('library-one') call read back, so it + // returned library-two's object and "firstLibrary.TextFunctions" was undefined. + var engine = new Engine(); + + const string libraryOne = @"(function () { + var $export = {}; + $export.TextFunctions = { generateRandomText: function (length) { return ''; } }; + return $export; + })();"; + + const string libraryTwo = @"(function () { + var $export = {}; + $export.DateFunctions = { getCurrentDate: function () { return new Date(); } }; + return $export; + })();"; + + engine.SetValue("importLibrary", new Func(name => name switch + { + "library-one" => engine.Evaluate(libraryOne), + "library-two" => engine.Evaluate(libraryTwo), + _ => JsValue.Undefined + })); + + engine.Modules.Add("module-one", "export async function fetchSamplePost() { return new Promise((resolve) => { }); }"); + engine.Modules.Add("module-two", @" + import * as bl from 'module-one'; + export async function sample() { + const lib2 = importLibrary('library-two'); + globalThis.currentDateFromLib2 = lib2.DateFunctions.getCurrentDate(); + return await bl.fetchSamplePost(); + }"); + + var result = engine.Evaluate(@" + var result = {}; + (async () => { + const m2 = await import('module-two'); + const obj = await m2.sample(); + result.obj = obj; + })(); + const firstLibrary = importLibrary('library-one'); + result.someText = firstLibrary.TextFunctions.generateRandomText(10); + result; + ").AsObject(); + + Assert.Equal("", result.Get("someText").AsString()); + } + [Fact] public void ShouldPropagateParseError() { diff --git a/Jint/Engine.Async.cs b/Jint/Engine.Async.cs index f1a2bb717..085a02bae 100644 --- a/Jint/Engine.Async.cs +++ b/Jint/Engine.Async.cs @@ -47,7 +47,7 @@ public Task EvaluateAsync(in Prepared