Skip to content

Commit 8ebce9a

Browse files
authored
Add extension point for custom Function.toString() logic (#2043)
1 parent e3c7fe7 commit 8ebce9a

File tree

5 files changed

+59
-9
lines changed

5 files changed

+59
-9
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using FluentAssertions;
2+
3+
namespace Jint.Tests.PublicInterface;
4+
5+
public class FunctionToStringTest
6+
{
7+
[Fact]
8+
public void CanRegisterCustomFunctionToString()
9+
{
10+
const string Code = "var x = 1; var y = 3; function testFunction() { return 'Something'; }; testFunction.toString(); var z = x + y;";
11+
12+
// we can rewrite back with AST to get custom formatting
13+
var engine = new Engine(options =>
14+
options.Host.FunctionToStringHandler = (function, node) => node.ToJavaScript(KnRJavaScriptTextFormatterOptions.Default)
15+
);
16+
17+
engine.Evaluate(Code).AsString().Should().Be($"function testFunction() {{{Environment.NewLine} return 'Something';{Environment.NewLine}}}");
18+
19+
// or we can brute force the original input when we use node's location information
20+
engine = new Engine(options =>
21+
options.Host.FunctionToStringHandler = (function, node) => Code.Substring(node.Start, node.End - node.Start)
22+
);
23+
24+
engine.Evaluate(Code).AsString().Should().Be("function testFunction() { return 'Something'; }");
25+
}
26+
}

Jint/Native/Function/Function.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,11 @@ internal void SetFunctionLength(JsNumber length)
368368

369369
public override string ToString()
370370
{
371-
// TODO no way to extract SourceText from Esprima at the moment, just returning native code
371+
if (_functionDefinition?.Function is Node node && _engine.Options.Host.FunctionToStringHandler(this, node) is { } s)
372+
{
373+
return s;
374+
}
375+
372376
var nameValue = _nameDescriptor != null ? UnwrapJsValue(_nameDescriptor) : JsString.Empty;
373377
var name = "";
374378
if (!nameValue.IsUndefined())
@@ -378,7 +382,7 @@ public override string ToString()
378382

379383
name = name.TrimStart(_functionNameTrimStartChars);
380384

381-
return "function " + name + "() { [native code] }";
385+
return $"function {name}() {{ [native code] }}";
382386
}
383387

384388
private sealed class ObjectInstanceWithConstructor : ObjectInstance

Jint/Options.Extensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public static Options LocalTimeZone(this Options options, TimeZoneInfo timeZoneI
106106
/// </summary>
107107
public static Options DisableStringCompilation(this Options options, bool disable = true)
108108
{
109-
options.StringCompilationAllowed = !disable;
109+
options.Host.StringCompilationAllowed = !disable;
110110
return options;
111111
}
112112

@@ -295,4 +295,4 @@ public static Options EnableModules(this Options options, IModuleLoader moduleLo
295295
options.Modules.ModuleLoader = moduleLoader;
296296
return options;
297297
}
298-
}
298+
}

Jint/Options.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Reflection;
55
using Jint.Native;
6+
using Jint.Native.Function;
67
using Jint.Native.Object;
78
using Jint.Runtime;
89
using Jint.Runtime.Interop;
@@ -47,7 +48,7 @@ public class Options
4748
/// <summary>
4849
/// Host options.
4950
/// </summary>
50-
internal HostOptions Host { get; } = new();
51+
public HostOptions Host { get; } = new();
5152

5253
/// <summary>
5354
/// Module options
@@ -64,7 +65,6 @@ public class Options
6465
/// </summary>
6566
public CultureInfo Culture { get; set; } = _defaultCulture;
6667

67-
6868
/// <summary>
6969
/// Configures a time system to use. Defaults to DefaultTimeSystem using local time.
7070
/// </summary>
@@ -94,7 +94,12 @@ public ITimeSystem TimeSystem
9494
/// <remarks>
9595
/// https://tc39.es/ecma262/#sec-hostensurecancompilestrings
9696
/// </remarks>
97-
public bool StringCompilationAllowed { get; set; } = true;
97+
[Obsolete("Use Options.Host.StringCompilationAllowed")]
98+
public bool StringCompilationAllowed
99+
{
100+
get => Host.StringCompilationAllowed;
101+
set => Host.StringCompilationAllowed = value;
102+
}
98103

99104
/// <summary>
100105
/// Options for the built-in JSON (de)serializer which
@@ -436,6 +441,21 @@ public class ConstraintOptions
436441
public class HostOptions
437442
{
438443
internal Func<Engine, Host> Factory { get; set; } = _ => new Host();
444+
445+
/// <summary>
446+
/// Whether calling 'eval' with custom code and function constructors taking function code as string is allowed.
447+
/// Defaults to true.
448+
/// </summary>
449+
/// <remarks>
450+
/// https://tc39.es/ecma262/#sec-hostensurecancompilestrings
451+
/// </remarks>
452+
public bool StringCompilationAllowed { get; set; } = true;
453+
454+
/// <summary>
455+
/// Possibility to override Jint's default function() { [native code] } format for functions using AST Node.
456+
/// If callback return null, Jint will use its own default logic.
457+
/// </summary>
458+
public Func<Function, Node, string?> FunctionToStringHandler { get; set; } = (_, _) => null;
439459
}
440460

441461
/// <summary>

Jint/Runtime/Host.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ protected virtual void CreateIntrinsics(Realm realmRec)
110110
/// </summary>
111111
public virtual void EnsureCanCompileStrings(Realm callerRealm, Realm evalRealm)
112112
{
113-
if (!Engine.Options.StringCompilationAllowed)
113+
if (!Engine.Options.Host.StringCompilationAllowed)
114114
{
115115
ExceptionHelper.ThrowJavaScriptException(callerRealm.Intrinsics.TypeError, "String compilation has been disabled in engine options");
116116
}
@@ -219,4 +219,4 @@ internal virtual List<string> GetSupportedImportAttributes()
219219
}
220220
}
221221

222-
internal sealed record JobCallback(ICallable Callback, object? HostDefined);
222+
internal sealed record JobCallback(ICallable Callback, object? HostDefined);

0 commit comments

Comments
 (0)