diff --git a/samples/Acornima.Cli/Commands/ParseCommand.cs b/samples/Acornima.Cli/Commands/ParseCommand.cs index 7c0230d..c39053c 100644 --- a/samples/Acornima.Cli/Commands/ParseCommand.cs +++ b/samples/Acornima.Cli/Commands/ParseCommand.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Acornima.Ast; using Acornima.Cli.Helpers; using Acornima.Jsx; @@ -46,16 +47,19 @@ public ParseCommand(IConsole console) [Option("-r|--range", Description = "Include range location information.")] public bool IncludeRange { get; set; } + [Option("--scopes", Description = "Include variable scope information. (Applies to simple overview only.)")] + public bool IncludeScopes { get; set; } + // TODO: more options [Argument(0, Description = "The JS code to parse. If omitted, the code will be read from the standard input.")] public string? Code { get; } - private T CreateParserOptions() where T : ParserOptions, new() => new T + private T CreateParserOptions(bool recordScopeInfo) where T : ParserOptions, new() => new T { RegExpParseMode = SkipRegExp ? RegExpParseMode.Skip : RegExpParseMode.Validate, Tolerant = Tolerant, - }; + }.RecordScopeInfoInUserData(recordScopeInfo); private T CreateAstToJsonOptions() where T : AstToJsonOptions, new() => new T { @@ -69,9 +73,11 @@ public int OnExecute() var code = Code ?? _console.ReadString(); + var recordScopeInfo = Simple && IncludeScopes; + IParser parser = AllowJsx - ? new JsxParser(CreateParserOptions()) - : new Parser(CreateParserOptions()); + ? new JsxParser(CreateParserOptions(recordScopeInfo)) + : new Parser(CreateParserOptions(recordScopeInfo)); Node rootNode = CodeType switch { @@ -83,15 +89,36 @@ public int OnExecute() if (Simple) { - var treePrinter = new TreePrinter(_console); - treePrinter.Print(new[] { rootNode }, - node => node.ChildNodes, - node => + Func getDisplayText = IncludeScopes + ? (node => { - var nodeType = node.Type.ToString(); + var nodeType = node.TypeText; + if (node.UserData is ScopeInfo scopeInfo) + { + var isHoistingScope = scopeInfo.AssociatedNode is IHoistingScope; + var names = scopeInfo.VarVariables.Select(id => id.Name) + .Concat(scopeInfo.LexicalVariables.Select(id => id.Name)) + .Concat(scopeInfo.Functions.Select(id => id.Name)) + .Distinct() + .OrderBy(name => name); + return $"{nodeType}{(isHoistingScope ? "*" : string.Empty)} [{string.Join(", ", names)}]"; + } + else + { + return nodeType; + } + }) + : (node => + { + var nodeType = node.TypeText; var nodeClrType = node.GetType().Name; - return nodeType == nodeClrType ? nodeType : $"{nodeType} ({nodeClrType})"; + return string.Equals(nodeType, nodeClrType, StringComparison.OrdinalIgnoreCase) ? nodeType : $"{nodeType} ({nodeClrType})"; }); + + var treePrinter = new TreePrinter(_console); + treePrinter.Print(new[] { rootNode }, + node => node.ChildNodes, + getDisplayText); } else { diff --git a/samples/Acornima.Cli/Commands/PrintScopesCommand.cs b/samples/Acornima.Cli/Commands/PrintScopesCommand.cs new file mode 100644 index 0000000..aecc383 --- /dev/null +++ b/samples/Acornima.Cli/Commands/PrintScopesCommand.cs @@ -0,0 +1,71 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using Acornima.Ast; +using Acornima.Cli.Helpers; +using Acornima.Jsx; +using McMaster.Extensions.CommandLineUtils; + +namespace Acornima.Cli.Commands; + + +[Command(CommandName, Description = "Parse JS code and print tree of variable scopes.")] +internal sealed class PrintScopesCommand +{ + public const string CommandName = "scopes"; + + private readonly IConsole _console; + + public PrintScopesCommand(IConsole console) + { + _console = console; + } + + [Option("--type", Description = "Type of the JS code to parse.")] + public JavaScriptCodeType CodeType { get; set; } + + [Option("--jsx", Description = "Allow JSX expressions.")] + public bool AllowJsx { get; set; } + + [Argument(0, Description = "The JS code to parse. If omitted, the code will be read from the standard input.")] + public string? Code { get; } + + private T CreateParserOptions() where T : ParserOptions, new() => new T().RecordScopeInfoInUserData(); + + public int OnExecute() + { + Console.InputEncoding = System.Text.Encoding.UTF8; + + var code = Code ?? _console.ReadString(); + + IParser parser = AllowJsx + ? new JsxParser(CreateParserOptions()) + : new Parser(CreateParserOptions()); + + Node rootNode = CodeType switch + { + JavaScriptCodeType.Script => parser.ParseScript(code), + JavaScriptCodeType.Module => parser.ParseModule(code), + JavaScriptCodeType.Expression => parser.ParseExpression(code), + _ => throw new InvalidOperationException() + }; + + var treePrinter = new TreePrinter(_console); + treePrinter.Print(new[] { rootNode }, + node => node + .DescendantNodes(descendIntoChildren: descendantNode => ReferenceEquals(node, descendantNode) || descendantNode.UserData is not ScopeInfo) + .Where(node => node.UserData is ScopeInfo), + node => + { + var scopeInfo = (ScopeInfo)node.UserData!; + var names = scopeInfo.VarVariables.Select(id => id.Name) + .Concat(scopeInfo.LexicalVariables.Select(id => id.Name)) + .Concat(scopeInfo.Functions.Select(id => id.Name)) + .Distinct() + .OrderBy(name => name); + return $"{node.TypeText} ({string.Join(", ", names)})"; + }); + + return 0; + } +} diff --git a/src/Acornima.Extras/ParserOptionsExtensions.cs b/src/Acornima.Extras/ParserOptionsExtensions.cs index 81ebddd..ba3e460 100644 --- a/src/Acornima.Extras/ParserOptionsExtensions.cs +++ b/src/Acornima.Extras/ParserOptionsExtensions.cs @@ -1,4 +1,6 @@ +using System; using Acornima.Ast; +using Acornima.Helpers; namespace Acornima; @@ -20,11 +22,38 @@ public static TOptions RecordParentNodeInUserData(this TOptions option return options; } + public static TOptions RecordScopeInfoInUserData(this TOptions options, bool enable = true) + where TOptions : ParserOptions + { + var helper = options._onNode?.Target as OnNodeHelper; + if (enable) + { + (helper ?? new OnNodeHelper()).EnableScopeInfoRecoding(options); + } + else + { + helper?.DisableScopeInfoRecoding(options); + } + + return options; + } + private sealed class OnNodeHelper : IOnNodeHandlerWrapper { private OnNodeHandler? _onNode; public OnNodeHandler? OnNode { get => _onNode; set => _onNode = value; } + private ArrayList _scopes; + + public void ReleaseLargeBuffers() + { + _scopes.Clear(); + if (_scopes.Capacity > 64) + { + _scopes.Capacity = 64; + } + } + public void EnableParentNodeRecoding(ParserOptions options) { if (!ReferenceEquals(options._onNode?.Target, this)) @@ -32,11 +61,44 @@ public void EnableParentNodeRecoding(ParserOptions options) _onNode = options._onNode; options._onNode = SetParentNode; } + else if (options._onNode == SetScopeInfo) + { + options._onNode = SetParentNodeAndScopeInfo; + } } public void DisableParentNodeRecoding(ParserOptions options) { - if (options._onNode == SetParentNode) + if (options._onNode == SetParentNodeAndScopeInfo) + { + options._onNode = SetScopeInfo; + } + else if (options._onNode == SetParentNode) + { + options._onNode = _onNode; + } + } + + public void EnableScopeInfoRecoding(ParserOptions options) + { + if (!ReferenceEquals(options._onNode?.Target, this)) + { + _onNode = options._onNode; + options._onNode = SetScopeInfo; + } + else if (options._onNode == SetParentNode) + { + options._onNode = SetParentNodeAndScopeInfo; + } + } + + public void DisableScopeInfoRecoding(ParserOptions options) + { + if (options._onNode == SetParentNodeAndScopeInfo) + { + options._onNode = SetParentNode; + } + else if (options._onNode == SetScopeInfo) { options._onNode = _onNode; } @@ -51,5 +113,113 @@ private void SetParentNode(Node node, OnNodeContext context) _onNode?.Invoke(node, context); } + + private void SetScopeInfo(Node node, OnNodeContext context) + { + if (context.HasScope) + { + SetScopeInfoCore(node, context._scope.Value, context.ScopeStack); + } + + _onNode?.Invoke(node, context); + } + + private void SetParentNodeAndScopeInfo(Node node, OnNodeContext context) + { + if (context.HasScope) + { + SetScopeInfoCore(node, context._scope.Value, context.ScopeStack); + } + + foreach (var child in node.ChildNodes) + { + if (child.UserData is ScopeInfo scopeInfo) + { + scopeInfo.UserData = node; + } + else + { + child.UserData = node; + } + } + + _onNode?.Invoke(node, context); + } + + private void SetScopeInfoCore(Node node, in Scope scope, ReadOnlySpan scopeStack) + { + for (var n = scope.Id - _scopes.Count; n >= 0; n--) + { + ref var scopeInfoRef = ref _scopes.PushRef(); + scopeInfoRef ??= new ScopeInfo(); + } + + var scopeInfo = _scopes.GetItemRef(scope.Id); + ref readonly var parentScope = ref scopeStack.Last(); + var parentScopeInfo = scope.Id != scopeStack[0].Id ? _scopes[parentScope.Id] : null; + + var varVariables = scope.VarVariables; + var lexicalVariables = scope.LexicalVariables; + Identifier? additionalLexicalVariable = null; + + // In the case of function and catch clause scopes, we need to create a separate scope for parameters, + // otherwise variables declared in the body would be "visible" to the parameter nodes. + + switch (node.Type) + { + case NodeType.CatchClause: + var catchClause = node.As(); + + node.UserData = parentScopeInfo = new ScopeInfo().Initialize( + node, + parent: parentScopeInfo, + varScope: _scopes[scopeStack[scope.CurrentVarScopeIndex].Id], + thisScope: _scopes[scopeStack[scope.CurrentThisScopeIndex].Id], + varVariables, + lexicalVariables: lexicalVariables.Slice(0, scope.LexicalParamCount), + functions: scope.Functions); + + node = catchClause.Body; + lexicalVariables = lexicalVariables.Slice(scope.LexicalParamCount); + break; + + case NodeType.ArrowFunctionExpression or NodeType.FunctionDeclaration or NodeType.FunctionExpression: + var function = node.As(); + var functionBody = function.Body as FunctionBody; + + node.UserData = parentScopeInfo = (functionBody is not null ? new ScopeInfo() : scopeInfo).Initialize( + node, + parent: parentScopeInfo, + varScope: _scopes[scopeStack[parentScope.CurrentVarScopeIndex].Id], + thisScope: _scopes[scopeStack[parentScope.CurrentThisScopeIndex].Id], + varVariables: varVariables.Slice(0, scope.VarParamCount), + lexicalVariables: default, + functions: default, + additionalVarVariable: function.Id); + + if (functionBody is null) + { + return; + } + + node = functionBody; + varVariables = varVariables.Slice(scope.VarParamCount); + break; + + case NodeType.ClassDeclaration or NodeType.ClassExpression: + additionalLexicalVariable = node.As()?.Id; + break; + } + + node.UserData = scopeInfo.Initialize( + node, + parent: parentScopeInfo, + varScope: scope.CurrentVarScopeIndex == scopeStack.Length ? scopeInfo : _scopes[scopeStack[scope.CurrentVarScopeIndex].Id], + thisScope: scope.CurrentThisScopeIndex == scopeStack.Length ? scopeInfo : _scopes[scopeStack[scope.CurrentThisScopeIndex].Id], + varVariables, + lexicalVariables, + functions: scope.Functions, + additionalLexicalVariable: additionalLexicalVariable); + } } } diff --git a/src/Acornima.Extras/ScopeInfo.cs b/src/Acornima.Extras/ScopeInfo.cs new file mode 100644 index 0000000..1db9c56 --- /dev/null +++ b/src/Acornima.Extras/ScopeInfo.cs @@ -0,0 +1,74 @@ +using System; +using System.Runtime.CompilerServices; +using Acornima.Ast; + +namespace Acornima; + +public sealed class ScopeInfo +{ + public static ScopeInfo From(Node associatedNode, + ScopeInfo? parent, ScopeInfo? varScope, ScopeInfo? thisScope, + ReadOnlySpan varVariables, ReadOnlySpan lexicalVariables, ReadOnlySpan functions) + { + var scope = new ScopeInfo(); + return scope.Initialize(associatedNode ?? throw new ArgumentNullException(nameof(associatedNode)), + parent, varScope ?? scope, thisScope ?? scope, + varVariables, lexicalVariables, functions); + } + + internal ScopeInfo() + { + AssociatedNode = null!; + VarScope = ThisScope = this; + } + + internal ScopeInfo Initialize(Node associatedNode, ScopeInfo? parent, ScopeInfo varScope, ScopeInfo thisScope, + ReadOnlySpan varVariables, ReadOnlySpan lexicalVariables, ReadOnlySpan functions, + Identifier? additionalVarVariable = null, Identifier? additionalLexicalVariable = null) + { + AssociatedNode = associatedNode; + Parent = parent; + VarScope = varScope; + ThisScope = thisScope; + VarVariables = new VariableCollection(varVariables, additionalVarVariable); + LexicalVariables = new VariableCollection(lexicalVariables, additionalLexicalVariable); + Functions = new VariableCollection(functions, additionalItem: null); + + return this; + } + + public Node AssociatedNode { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; private set; } + + public ScopeInfo? Parent { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; private set; } + public ScopeInfo VarScope { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; private set; } + public ScopeInfo ThisScope { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; private set; } + + /// + /// A list of distinct var-declared names sorted in ascending order in the current lexical scope. + /// + public VariableCollection VarVariables { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; private set; } + + /// + /// A list of distinct lexically-declared names sorted in ascending order in the current lexical scope. + /// + public VariableCollection LexicalVariables { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; private set; } + + /// + /// A list of distinct lexically-declared names sorted in ascending order in the current lexical scope. + /// + public VariableCollection Functions { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; private set; } + + /// + /// Gets or sets the arbitrary, user-defined data object associated with the current . + /// + /// + /// The operation is not guaranteed to be thread-safe. In case concurrent access or update is possible, the necessary synchronization is caller's responsibility. + /// + public object? UserData + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set; + } +} diff --git a/src/Acornima.Extras/VariableCollection.cs b/src/Acornima.Extras/VariableCollection.cs new file mode 100644 index 0000000..0e07731 --- /dev/null +++ b/src/Acornima.Extras/VariableCollection.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using Acornima.Ast; + +namespace Acornima; + +[DebuggerDisplay($"{nameof(Count)} = {{{nameof(Count)}}}")] +[DebuggerTypeProxy(typeof(DebugView))] +public readonly struct VariableCollection : IReadOnlyCollection +{ + private readonly Identifier[]? _items; + + internal VariableCollection(ReadOnlySpan items, Identifier? additionalItem) + { + if (additionalItem is not null) + { + if (items.Length == 0) + { + _items = new[] { additionalItem }; + return; + } + _items = new Identifier[items.Length + 1]; + _items[0] = additionalItem; + items.CopyTo(_items.AsSpan(1)); + } + else + { + if (items.Length == 0) + { + return; + } + _items = items.ToArray(); + } + + Array.Sort(_items, NameComparer.Instance); + } + + public VariableCollection(ReadOnlySpan items) + : this(items, additionalItem: null) { } + + public VariableCollection(IEnumerable items) + { + _items = items.ToArray(); + Array.Sort(_items, NameComparer.Instance); + } + + public int Count { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _items?.Length ?? 0; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(Identifier item) + { + return _items is not null && Array.IndexOf(_items, item) >= 0; + } + + public bool Contains(string name) + { + for (int lo = 0, hi = Count - 1; lo <= hi;) + { + var i = lo + ((hi - lo) >> 1); + var order = string.CompareOrdinal(_items![i].Name, name); + if (order < 0) + { + lo = i + 1; + } + else if (order > 0) + { + hi = i - 1; + } + else + { + return true; + } + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public struct Enumerator : IEnumerator + { + private readonly Identifier[]? _items; + private readonly int _count; + private int _index; + + internal Enumerator(VariableCollection list) + { + _items = list._items; + _count = _items?.Length ?? 0; + _index = -1; + } + + public readonly void Dispose() { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + var index = _index + 1; + if (index < _count) + { + _index = index; + return true; + } + + return false; + } + + public void Reset() + { + _index = -1; + } + + /// + /// According to the specification, + /// accessing before calling or after returning is undefined behavior. + /// Thus, to maximize performance, this implementation doesn't do any null or range checks, just let the default exceptions occur on invalid access. + /// + public readonly Identifier Current { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _items![_index]; } + + readonly object? IEnumerator.Current => Current; + } + + private sealed class NameComparer : IComparer + { + public static readonly NameComparer Instance = new(); + + private NameComparer() { } + + public int Compare(Identifier? x, Identifier? y) => string.CompareOrdinal(x!.Name, y!.Name); + } + + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly VariableCollection _collection; + + public DebugView(VariableCollection collection) + { + _collection = collection; + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public Identifier[] Items => _collection.ToArray(); + } +} diff --git a/src/Acornima/Parser.State.cs b/src/Acornima/Parser.State.cs index 5e91077..8d7f19e 100644 --- a/src/Acornima/Parser.State.cs +++ b/src/Acornima/Parser.State.cs @@ -146,6 +146,8 @@ private void ReleaseLargeBuffers() } _tokenizer.ReleaseLargeBuffers(); + + (_options._onNode?.Target as IOnNodeHandlerWrapper)?.ReleaseLargeBuffers(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Acornima/ParserOptions.cs b/src/Acornima/ParserOptions.cs index e1ce502..69280a1 100644 --- a/src/Acornima/ParserOptions.cs +++ b/src/Acornima/ParserOptions.cs @@ -18,6 +18,8 @@ namespace Acornima; internal interface IOnNodeHandlerWrapper { OnNodeHandler? OnNode { get; set; } + + void ReleaseLargeBuffers(); } public record class ParserOptions diff --git a/test/Acornima.Tests/ParserTests.cs b/test/Acornima.Tests/ParserTests.cs index f660ebb..e71c437 100644 --- a/test/Acornima.Tests/ParserTests.cs +++ b/test/Acornima.Tests/ParserTests.cs @@ -213,25 +213,128 @@ public void CanReuseParser() } [Theory] - [InlineData(false)] - [InlineData(true)] - public void RecordsParentNodeInUserDataCorrectly(bool registerUserHandler) + [InlineData(false, false, false)] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(false, true, true)] + [InlineData(true, false, false)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + [InlineData(true, true, true)] + public void RecordsParentNodeInUserDataCorrectly(bool registerUserHandler, bool enableScopeInfoRecording, bool disableScopeInfoRecording) { var userHandlerCalled = false; var options = (registerUserHandler ? new ParserOptions { OnNode = delegate { userHandlerCalled = true; } } : new ParserOptions()) .RecordParentNodeInUserData(); + if (enableScopeInfoRecording) + { + options = options.RecordScopeInfoInUserData(); + } + if (disableScopeInfoRecording) + { + options = options.RecordScopeInfoInUserData(enable: false); + } + var parser = new Parser(options); var script = parser.ParseScript("function toObj(a, b) { return { a, b() { return b } }; }"); - Func parentGetter = node => (Node?)node.UserData; + Func parentGetter = !enableScopeInfoRecording || disableScopeInfoRecording + ? node => (Node?)node.UserData + : node => (Node?)(node.UserData is ScopeInfo scopeInfo ? scopeInfo.UserData : node.UserData); new ParentNodeChecker(parentGetter).Check(script); Assert.Equal(registerUserHandler, userHandlerCalled); } + [Theory] + [InlineData(false, false, false)] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(false, true, true)] + [InlineData(true, false, false)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + [InlineData(true, true, true)] + public void RecordsScopeInfoInUserDataCorrectly(bool registerUserHandler, bool enableParentNodeRecording, bool disableParentNodeRecording) + { + var userHandlerCalled = false; + + var options = (registerUserHandler ? new ParserOptions { OnNode = delegate { userHandlerCalled = true; } } : new ParserOptions()) + .RecordScopeInfoInUserData(); + + if (enableParentNodeRecording) + { + options = options.RecordParentNodeInUserData(); + } + if (disableParentNodeRecording) + { + options = options.RecordParentNodeInUserData(enable: false); + } + + var parser = new Parser(options); + var script = parser.ParseScript("function toObj(a, b) { return { a, b: x => { let y = 2; return x * y } }; }"); + + var nodesWithScopes = script.DescendantNodesAndSelf() + .Select(node => node.UserData) + .OfType() + .ToArray(); + + Assert.Equal(5, nodesWithScopes.Length); + + Assert.Same(script, nodesWithScopes[0].AssociatedNode); + Assert.Null(nodesWithScopes[0].Parent); + Assert.Same(nodesWithScopes[0], nodesWithScopes[0].VarScope); + Assert.Same(nodesWithScopes[0], nodesWithScopes[0].ThisScope); + Assert.Equal(new[] { "toObj" }, nodesWithScopes[0].Functions.Select(id => id.Name)); + Assert.Empty(nodesWithScopes[0].LexicalVariables); + Assert.Empty(nodesWithScopes[0].VarVariables); + + var functionDeclaration = script.Body[0].As(); + Assert.Same(functionDeclaration, nodesWithScopes[1].AssociatedNode); + Assert.Same(nodesWithScopes[0], nodesWithScopes[1].Parent); + Assert.Same(nodesWithScopes[0], nodesWithScopes[1].VarScope); + Assert.Same(nodesWithScopes[0], nodesWithScopes[1].ThisScope); + Assert.Empty(nodesWithScopes[1].Functions); + Assert.Empty(nodesWithScopes[1].LexicalVariables); + Assert.Equal(new[] { "a", "b", "toObj" }, nodesWithScopes[1].VarVariables.Select(id => id.Name)); + + var functionBody = functionDeclaration.Body; + Assert.Same(functionBody, nodesWithScopes[2].AssociatedNode); + Assert.Same(nodesWithScopes[1], nodesWithScopes[2].Parent); + Assert.Same(nodesWithScopes[2], nodesWithScopes[2].VarScope); + Assert.Same(nodesWithScopes[2], nodesWithScopes[2].ThisScope); + Assert.Empty(nodesWithScopes[2].Functions); + Assert.Empty(nodesWithScopes[2].LexicalVariables); + Assert.Empty(nodesWithScopes[2].VarVariables); + + var arrowFunctionExpression = functionBody.Body[0] + .As().Argument! + .As().Properties[1] + .As().Value + .As(); + Assert.Same(arrowFunctionExpression, nodesWithScopes[3].AssociatedNode); + Assert.Same(nodesWithScopes[2], nodesWithScopes[3].Parent); + Assert.Same(nodesWithScopes[2], nodesWithScopes[3].VarScope); + Assert.Same(nodesWithScopes[2], nodesWithScopes[3].ThisScope); + Assert.Empty(nodesWithScopes[3].Functions); + Assert.Empty(nodesWithScopes[3].LexicalVariables); + Assert.Equal(new[] { "x" }, nodesWithScopes[3].VarVariables.Select(id => id.Name)); + + functionBody = arrowFunctionExpression.Body.As(); + Assert.Same(functionBody, nodesWithScopes[4].AssociatedNode); + Assert.Same(nodesWithScopes[3], nodesWithScopes[4].Parent); + Assert.Same(nodesWithScopes[4], nodesWithScopes[4].VarScope); + Assert.Same(nodesWithScopes[2], nodesWithScopes[4].ThisScope); + Assert.Empty(nodesWithScopes[4].Functions); + Assert.Equal(new[] { "y" }, nodesWithScopes[4].LexicalVariables.Select(id => id.Name)); + Assert.Empty(nodesWithScopes[4].VarVariables); + + Assert.Equal(registerUserHandler, userHandlerCalled); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -248,14 +351,42 @@ public void ShouldPreserveUserOnNodeHandler(bool registerUserHandler) options = options.RecordParentNodeInUserData(); Assert.Same(userHandler, options.OnNode); + options = options.RecordScopeInfoInUserData(); + Assert.Same(userHandler, options.OnNode); + options = options.RecordParentNodeInUserData(enable: false); Assert.Same(userHandler, options.OnNode); + options = options.RecordScopeInfoInUserData(enable: false); + Assert.Same(userHandler, options.OnNode); + var parser = new Parser(options); var script = parser.ParseScript(code); Assert.Empty(script.DescendantNodesAndSelf().Where(node => node.UserData is not null)); Assert.Equal(registerUserHandler, userHandlerCalled); + + userHandlerCalled = false; + + Assert.Same(userHandler, options.OnNode); + + options = options.RecordScopeInfoInUserData(); + Assert.Same(userHandler, options.OnNode); + + options = options.RecordParentNodeInUserData(); + Assert.Same(userHandler, options.OnNode); + + options = options.RecordScopeInfoInUserData(enable: false); + Assert.Same(userHandler, options.OnNode); + + options = options.RecordParentNodeInUserData(enable: false); + Assert.Same(userHandler, options.OnNode); + + parser = new Parser(options); + script = parser.ParseScript(code); + + Assert.Empty(script.DescendantNodesAndSelf().Where(node => node.UserData is not null)); + Assert.Equal(registerUserHandler, userHandlerCalled); } [Theory]