Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
393 changes: 393 additions & 0 deletions Jint.Tests/Runtime/FunctionTests.cs

Large diffs are not rendered by default.

24 changes: 22 additions & 2 deletions Jint/AstExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using System.Threading;
using Jint.Native;
using Jint.Native.Function;
using Jint.Native.Object;
Expand All @@ -17,6 +18,8 @@ public static class AstExtensions
internal static readonly SourceLocation DefaultLocation;
#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value

private static Tokenizer? s_cachedTokenizer;

public static JsValue GetKey<T>(this T property, Engine engine) where T : IProperty => GetKey(property.Key, engine, property.Computed);

public static JsValue GetKey(this Expression expression, Engine engine, bool resolveComputed = false)
Expand Down Expand Up @@ -325,7 +328,7 @@ internal static void BindingInitialization(
/// <summary>
/// https://tc39.es/ecma262/#sec-runtime-semantics-definemethod
/// </summary>
internal static Record DefineMethod<T>(this T m, ObjectInstance obj, ObjectInstance? functionPrototype = null) where T : IProperty
internal static Record DefineMethod<T>(this T m, ObjectInstance obj, ObjectInstance? functionPrototype, INode sourceTextNode) where T : IProperty
{
var engine = obj.Engine;
var propKey = TypeConverter.ToPropertyKey(m.GetKey(engine));
Expand All @@ -342,7 +345,7 @@ internal static Record DefineMethod<T>(this T m, ObjectInstance obj, ObjectInsta
Throw.SyntaxError(engine.Realm);
}

var definition = new JintFunctionDefinition(function);
var definition = new JintFunctionDefinition(function, sourceTextNode);
var closure = intrinsics.Function.OrdinaryFunctionCreate(prototype, definition, definition.ThisMode, env, privateEnv);
closure.MakeMethod(obj);

Expand Down Expand Up @@ -523,6 +526,23 @@ internal static DisposeHint GetDisposeHint(this VariableDeclarationKind statemen
};
}

internal static int GetSecondTokenStartIndex(string sourceText, int start, int end)
{
var tokenizer = Interlocked.Exchange(ref s_cachedTokenizer, value: null) ?? new Tokenizer(string.Empty);
try
{
tokenizer.Reset(sourceText, start, end - start, SourceType.Script);
tokenizer.Next();
tokenizer.Next(); // skip first token + potential whitespace and/or comments
return tokenizer.Current.Start;
}
finally
{
tokenizer.Reset(string.Empty);
Volatile.Write(ref s_cachedTokenizer, tokenizer);
}
}

private sealed class MinimalSyntaxElement : Node
{
public MinimalSyntaxElement(in SourceLocation location) : base(NodeType.Unknown)
Expand Down
22 changes: 7 additions & 15 deletions Jint/Engine.Ast.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,17 @@ public static Prepared<Script> PrepareScript(string code, string? source = null,
source ??= "<anonymous>";
options ??= ScriptPreparationOptions.Default;

var sourceOffset = options.ParsingOptions.SourceOffset;
var padding = AcornimaExtensions.CreateSourceOffsetPadding(sourceOffset);
var paddedCode = padding.Length > 0 ? padding + code : code;

var astAnalyzer = new AstAnalyzer(options);
var parserOptions = options.GetParserOptions();
var parser = new Parser(parserOptions with { OnNode = astAnalyzer.NodeVisitor });

try
{
Script preparedScript;
var sourceOffset = options.ParsingOptions.SourceOffset;
var padding = AcornimaExtensions.CreateSourceOffsetPadding(sourceOffset);

if (padding.Length > 0)
{
var paddedCode = padding + code;
preparedScript = parser.ParseScript(paddedCode, padding.Length, code.Length, source, strict);
}
else
{
preparedScript = parser.ParseScript(code, source, strict);
}
var preparedScript = parser.ParseScript(paddedCode, padding.Length, code.Length, source, strict);

return new Prepared<Script>(preparedScript, parserOptions);
}
Expand Down Expand Up @@ -85,7 +77,7 @@ public AstAnalyzer(IPreparationOptions<IParsingOptions> preparationOptions)
_preparationOptions = preparationOptions;
}

public void NodeVisitor(Node node, in OnNodeContext _)
public void NodeVisitor(Node node, in OnNodeContext ctx)
{
switch (node.Type)
{
Expand Down Expand Up @@ -115,7 +107,7 @@ public void NodeVisitor(Node node, in OnNodeContext _)
case NodeType.ArrowFunctionExpression:
case NodeType.FunctionDeclaration:
case NodeType.FunctionExpression:
node.UserData = JintFunctionDefinition.BuildState((IFunction) node);
node.UserData = JintFunctionDefinition.BuildState((IFunction) node, ctx.Input);
break;

case NodeType.Program:
Expand Down
14 changes: 14 additions & 0 deletions Jint/Engine.Defaults.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Jint.Native.Function;

namespace Jint;

public partial class Engine
Expand Down Expand Up @@ -45,4 +47,16 @@ internal RegExpParseResult HandleOnRegExp(in RegExpParsingContext ctx)
return RegExpParseResult.ForSuccess(additionalData: this);
}
}

/// <summary>
/// Cached OnNode callback that stores the source text being parsed in <see cref="Node.UserData"/> of function nodes
/// to support <see cref="Function.ToString"><c>Function.prototype.toString()</c> implementation</see>.
/// </summary>
internal static readonly OnNodeHandler DefaultNodeHandler = static (node, in ctx) =>
{
if (node.Type is NodeType.ArrowFunctionExpression or NodeType.FunctionDeclaration or NodeType.FunctionExpression)
{
node.UserData = ctx.Input;
}
};
}
16 changes: 10 additions & 6 deletions Jint/Extensions/AcornimaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ internal static class AcornimaExtensions
public static Script ParseScriptGuarded(this Parser parser, Realm realm, string code, Position sourceOffset = default, string? source = null, bool strict = false)
{
var padding = CreateSourceOffsetPadding(sourceOffset);
var paddedCode = padding.Length > 0 ? padding + code : code;

try
{
return padding.Length > 0
? parser.ParseScript(padding + code, padding.Length, code.Length, source, strict)
: parser.ParseScript(code, source, strict);
return parser.ParseScript(paddedCode, padding.Length, code.Length, source, strict);
}
catch (ParseErrorException e)
{
Expand All @@ -26,9 +25,14 @@ public static Script ParseScriptGuarded(this Parser parser, Realm realm, string
/// </summary>
internal static string CreateSourceOffsetPadding(Position sourceOffset)
{
var lineOffset = sourceOffset.Line > 0 ? sourceOffset.Line - 1 : 0;
var columnOffset = sourceOffset.Column > 0 ? sourceOffset.Column : 0;
return new string('\n', lineOffset) + new string(' ', columnOffset);
if (sourceOffset.Line > 0)
{
var lineOffset = sourceOffset.Line - 1;
var columnOffset = sourceOffset.Column > 0 ? sourceOffset.Column : 0;
return new string('\n', lineOffset) + new string(' ', columnOffset);
}

return string.Empty;
}

public static Module ParseModuleGuarded(this Parser parser, Engine engine, string code, string? source = null)
Expand Down
26 changes: 12 additions & 14 deletions Jint/Native/Function/ClassDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,16 @@ static MethodDefinition CreateConstructorMethodDefinition(Parser parser, string
_emptyConstructor = CreateConstructorMethodDefinition(parser, "class temp { constructor() {} }");
}

private readonly NodeList<Decorator> _classDecorators;
private readonly IClass _class;

public ClassDefinition(
string? className,
Expression? superClass,
ClassBody body,
in NodeList<Decorator> classDecorators = default)
IClass @class)
{
_className = className;
_superClass = superClass;
_body = body;
_classDecorators = classDecorators;
_superClass = @class.SuperClass;
_body = @class.Body;
_class = @class;
}

/// <summary>
Expand Down Expand Up @@ -79,7 +77,7 @@ public JsValue BuildConstructor(EvaluationContext context, Environment env, stri
if (hasDecorators)
{
// Evaluate class-level decorators
classDecoratorValues = EvaluateDecorators(context, _classDecorators);
classDecoratorValues = EvaluateDecorators(context, _class.Decorators);

// Evaluate element-level decorators
ref readonly var bodyElements = ref _body.Body;
Expand Down Expand Up @@ -183,7 +181,7 @@ public JsValue BuildConstructor(EvaluationContext context, Environment env, stri
ScriptFunction F;
try
{
var constructorInfo = constructor.DefineMethod(proto, constructorParent);
var constructorInfo = constructor.DefineMethod(proto, constructorParent, sourceTextNode: _class);
F = constructorInfo.Closure;

F.SetFunctionName(_className ?? classBinding ?? "");
Expand Down Expand Up @@ -761,14 +759,12 @@ public ClassStaticBlockFunction(StaticBlock staticBlock) : base(NodeType.StaticB

if (method.Kind != PropertyKind.Get && method.Kind != PropertyKind.Set && !function.Generator)
{
var methodDef = method.DefineMethod(obj);
var methodDef = method.DefineMethod(obj, functionPrototype: null, sourceTextNode: method);
methodDef.Closure.SetFunctionName(methodDef.Key);
return DefineMethodProperty(obj, methodDef.Key, methodDef.Closure, enumerable);
}

var getter = method.Kind == PropertyKind.Get;

var definition = new JintFunctionDefinition(function);
var definition = new JintFunctionDefinition(function, sourceTextNode: method);
var intrinsics = engine.Realm.Intrinsics;

var value = method.TryGetKey(engine);
Expand Down Expand Up @@ -803,6 +799,8 @@ public ClassStaticBlockFunction(StaticBlock staticBlock) : base(NodeType.StaticB
}
else
{
var getter = method.Kind == PropertyKind.Get;

var closure = intrinsics.Function.OrdinaryFunctionCreate(intrinsics.Function.PrototypeObject, definition, definition.ThisMode, env, privateEnv);
closure.MakeMethod(obj);
closure.SetFunctionName(propKey, getter ? "get" : "set");
Expand Down Expand Up @@ -1092,7 +1090,7 @@ private static AccessorDecoratorResult ApplyAccessorDecorators(
/// </summary>
private bool HasDecorators()
{
if (_classDecorators.Count > 0) return true;
if (_class.Decorators.Count > 0) return true;

ref readonly var elements = ref _body.Body;
for (var i = 0; i < elements.Count; i++)
Expand Down
14 changes: 12 additions & 2 deletions Jint/Native/Function/Function.cs
Original file line number Diff line number Diff line change
Expand Up @@ -375,9 +375,19 @@ public sealed override object ToObject()

public override string ToString()
{
if (_functionDefinition?.Function is Node node && _engine.Options.Host.FunctionToStringHandler(this, node) is { } s)
if (_functionDefinition is not null)
{
return s;
var sourceTextNode = (Node) _functionDefinition.SourceTextNode;
if (_engine.Options.Host.FunctionToStringHandler(this, sourceTextNode) is { } s)
{
return s;
}

var state = _functionDefinition.Initialize();
if (state.SourceText.GetValue(sourceTextNode) is { } sourceText)
{
return sourceText;
}
}

var nameValue = _nameDescriptor != null ? UnwrapJsValue(_nameDescriptor) : JsString.Empty;
Expand Down
18 changes: 9 additions & 9 deletions Jint/Native/Function/FunctionInstance.Dynamic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,16 @@ internal Function CreateDynamicFunction(
switch (kind)
{
case FunctionKind.Normal:
functionExpression = "function f(){}";
functionExpression = "function anonymous(\n) {\n\n}";
break;
case FunctionKind.Generator:
functionExpression = "function* f(){}";
functionExpression = "function* anonymous(\n) {\n\n}";
break;
case FunctionKind.Async:
functionExpression = "async function f(){}";
functionExpression = "async function anonymous(\n) {\n\n}";
break;
case FunctionKind.AsyncGenerator:
functionExpression = "async function* f(){}";
functionExpression = "async function* anonymous(\n) {\n\n}";
break;
default:
Throw.ArgumentOutOfRangeException(nameof(kind), kind.ToString());
Expand All @@ -105,16 +105,16 @@ internal Function CreateDynamicFunction(
switch (kind)
{
case FunctionKind.Normal:
functionExpression = "function f(";
functionExpression = "function anonymous(";
break;
case FunctionKind.Async:
functionExpression = "async function f(";
functionExpression = "async function anonymous(";
break;
case FunctionKind.Generator:
functionExpression = "function* f(";
functionExpression = "function* anonymous(";
break;
case FunctionKind.AsyncGenerator:
functionExpression = "async function* f(";
functionExpression = "async function* anonymous(";
break;
default:
Throw.ArgumentOutOfRangeException(nameof(kind), kind.ToString());
Expand All @@ -124,7 +124,7 @@ internal Function CreateDynamicFunction(
// Per spec (CreateDynamicFunction step 29), a line feed follows the parameters,
// and the body is wrapped with line feeds (step 16). This ensures HTML-like
// comments (<!-- and -->) are correctly handled as line comments.
functionExpression += p + "\n){\n" + body + "\n}";
functionExpression += p + "\n) {\n" + body + "\n}";
}

var parserOptions = _engine.GetActiveParserOptions();
Expand Down
47 changes: 47 additions & 0 deletions Jint/Native/Function/SourceText.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Runtime.CompilerServices;

namespace Jint.Native.Function
{
internal struct SourceText
{
// Either a string that stores the full source text that the function was parsed from,
// or a StrongBox<string> containing the materialized source text of the function,
// or null when the function doesn't have source text.
private object? _fullSourceTextOrBoxedValue;

public SourceText(string? fullSourceText)
{
_fullSourceTextOrBoxedValue = fullSourceText;
}

public string? GetValue(Node sourceTextNode)
{
if (_fullSourceTextOrBoxedValue is null)
{
return null;
}

if (_fullSourceTextOrBoxedValue is StrongBox<string> boxedValue)
{
return boxedValue.Value;
}

var fullSourceText = (string) _fullSourceTextOrBoxedValue;

var (start, end) = sourceTextNode.Range;

// In the case of static class methods, the static keyword is not included.
if (sourceTextNode is MethodDefinition { Static: true } m)
{
start = AstExtensions.GetSecondTokenStartIndex(fullSourceText, start, end);
}

var value = fullSourceText.Substring(start, end - start);

// Setting this field also releases the reference to the full source text string instance.
_fullSourceTextOrBoxedValue = new StrongBox<string>(value);

return value;
}
}
}
2 changes: 2 additions & 0 deletions Jint/Native/ShadowRealm/ShadowRealm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,8 @@ protected internal override JsValue Call(JsValue thisArgument, JsCallArguments a

return GetWrappedValue(callerRealm, callerRealm, result);
}

public override string ToString() => _wrappedTargetFunction.ToString();
}

/// <summary>
Expand Down
14 changes: 12 additions & 2 deletions Jint/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -495,9 +495,19 @@ public class HostOptions
public bool StringCompilationAllowed { get; set; } = true;

/// <summary>
/// Possibility to override Jint's default function() { [native code] } format for functions using AST Node.
/// If callback return null, Jint will use its own default logic.
/// Possibility to override Jint's default <c>Function.prototype.toString()</c> implementation.
/// If the callback returns <see langword="null"/>, Jint will use its own default logic.
/// </summary>
/// <remarks>
/// In most cases, the AST node passed to the callback is of type <see cref="IFunction" />.
/// However, there are some special cases:<br/>
/// - For class constructors, a node of type <see cref="IClass"/> is passed
/// since <c>toString()</c> should return the code of the entire class as per specification.<br/>
/// - For class methods and getters/setters, a node of type <see cref="MethodDefinition"/> is passed
/// since <c>toString()</c> should include the <c>get</c> or <c>set</c> tokens for getters/setters and the member name as per specification.<br/>
/// - For object methods and getters/setters, a node of type <see cref="ObjectProperty"/> is passed
/// since <c>toString()</c> should include the <c>get</c> or <c>set</c> tokens for getters/setters and the member name as per specification.
/// </remarks>
public Func<Function, Node, string?> FunctionToStringHandler { get; set; } = (_, _) => null;
}

Expand Down
Loading