From c18fdda854212a5f6c2fab28a846edd062810d2b Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Thu, 20 Aug 2026 22:06:32 +0300 Subject: [PATCH] Never read a pending lazy name descriptor when rendering an error message GetOwnFunctionNameForMessage read _nameDescriptor?.Value under a doc comment claiming the read touches the raw field and never invokes an accessor. That is false for a descriptor carrying PropertyFlag.CustomJsValue: Value routes through the CustomValue accessor, and a script function's own name starts as the shared pending-lazy sentinel whose accessors throw by design. Rendering an error message for a function whose name was never materialized could therefore itself throw InvalidOperationException - a latent throw inside error-message construction, the one place that must never produce one. The method now skips any CustomJsValue descriptor and falls back to the CLR type name, which is also the right answer for the message's purpose: only a plain materialized string was ever quoted. The doc comment is corrected to say why. Closes #3112 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011G7Ud48VAs1JicxRZxLDxg --- .../Runtime/FunctionNameForMessageTests.cs | 36 +++++++++++++++++++ Jint/Native/Function/Function.cs | 15 ++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 Jint.Tests/Runtime/FunctionNameForMessageTests.cs diff --git a/Jint.Tests/Runtime/FunctionNameForMessageTests.cs b/Jint.Tests/Runtime/FunctionNameForMessageTests.cs new file mode 100644 index 0000000000..2360b74712 --- /dev/null +++ b/Jint.Tests/Runtime/FunctionNameForMessageTests.cs @@ -0,0 +1,36 @@ +using Jint.Native.Function; + +namespace Jint.Tests.Runtime; + +/// +/// exists to render a name inside an error message +/// without running script — so it must never throw itself. A script function's own name starts as +/// the shared pending-lazy sentinel, whose value accessors throw by design to make a leak loud; reading +/// the descriptor's Value before anything materialized the property is exactly such a leak. +/// +public class FunctionNameForMessageTests +{ + [Fact] + public void APendingLazyNameFallsBackToTheClrTypeNameInsteadOfThrowing() + { + using var engine = new Engine(); + + // The function's own "name" property exists from birth, but its descriptor is the pending + // sentinel until something reads the property — which nothing here does. + var function = (Function) engine.Evaluate("(function foo() {})"); + + var name = function.GetOwnFunctionNameForMessage(); + + Assert.Equal(function.GetType().Name, name); + } + + [Fact] + public void AMaterializedNameIsQuoted() + { + using var engine = new Engine(); + + var function = (Function) engine.Evaluate("const f = function foo() {}; void f.name; f"); + + Assert.Equal("foo", function.GetOwnFunctionNameForMessage()); + } +} diff --git a/Jint/Native/Function/Function.cs b/Jint/Native/Function/Function.cs index e835394020..172344b5e2 100644 --- a/Jint/Native/Function/Function.cs +++ b/Jint/Native/Function/Function.cs @@ -663,13 +663,22 @@ internal string GetOwnFunctionName() /// toString throws. coerces through TypeConverter.ToString /// and would let that object hijack the error being built — an extra observable call, and a thrown /// value replacing the TypeError the caller meant to raise. So only an actual string is quoted - /// here; anything else falls back to the CLR type's name, which no script can influence. Reading - /// touches the raw field and never invokes an accessor. + /// here; anything else falls back to the CLR type's name, which no script can influence. A descriptor + /// carrying is skipped for the same reason: its + /// routes through the CustomValue accessor rather than the + /// raw field, and one of those accessors — the shared pending-lazy sentinel a script function's name + /// starts with — throws by design. An error-message path must never introduce a throw of its own. /// /// internal string GetOwnFunctionNameForMessage() { - return _nameDescriptor?.Value is JsString name ? name.ToString() : GetType().Name; + var descriptor = _nameDescriptor; + if (descriptor is null || (descriptor.Flags & PropertyFlag.CustomJsValue) != PropertyFlag.None) + { + return GetType().Name; + } + + return descriptor.Value is JsString name ? name.ToString() : GetType().Name; } ///