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; } ///