Skip to content

Commit

Permalink
Introduce convenience API (RecordScopeInfoInUserData, ScopeInfo) for …
Browse files Browse the repository at this point in the history
…dealing with variable scopes
  • Loading branch information
adams85 committed Jun 22, 2024
1 parent 7ccea95 commit e1004c6
Show file tree
Hide file tree
Showing 8 changed files with 646 additions and 15 deletions.
47 changes: 37 additions & 10 deletions samples/Acornima.Cli/Commands/ParseCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using Acornima.Ast;
using Acornima.Cli.Helpers;
using Acornima.Jsx;
Expand Down Expand Up @@ -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<T>() where T : ParserOptions, new() => new T
private T CreateParserOptions<T>(bool recordScopeInfo) where T : ParserOptions, new() => new T
{
RegExpParseMode = SkipRegExp ? RegExpParseMode.Skip : RegExpParseMode.Validate,
Tolerant = Tolerant,
};
}.RecordScopeInfoInUserData(recordScopeInfo);

private T CreateAstToJsonOptions<T>() where T : AstToJsonOptions, new() => new T
{
Expand All @@ -69,9 +73,11 @@ public int OnExecute()

var code = Code ?? _console.ReadString();

var recordScopeInfo = Simple && IncludeScopes;

IParser parser = AllowJsx
? new JsxParser(CreateParserOptions<JsxParserOptions>())
: new Parser(CreateParserOptions<ParserOptions>());
? new JsxParser(CreateParserOptions<JsxParserOptions>(recordScopeInfo))
: new Parser(CreateParserOptions<ParserOptions>(recordScopeInfo));

Node rootNode = CodeType switch
{
Expand All @@ -83,15 +89,36 @@ public int OnExecute()

if (Simple)
{
var treePrinter = new TreePrinter(_console);
treePrinter.Print(new[] { rootNode },
node => node.ChildNodes,
node =>
Func<Node, string> 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
{
Expand Down
71 changes: 71 additions & 0 deletions samples/Acornima.Cli/Commands/PrintScopesCommand.cs
Original file line number Diff line number Diff line change
@@ -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<T>() 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<JsxParserOptions>())
: new Parser(CreateParserOptions<ParserOptions>());

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;
}
}
172 changes: 171 additions & 1 deletion src/Acornima.Extras/ParserOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using Acornima.Ast;
using Acornima.Helpers;

namespace Acornima;

Expand All @@ -20,23 +22,83 @@ public static TOptions RecordParentNodeInUserData<TOptions>(this TOptions option
return options;
}

public static TOptions RecordScopeInfoInUserData<TOptions>(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<ScopeInfo> _scopes;

public void ReleaseLargeBuffers()
{
_scopes.Clear();
if (_scopes.Capacity > 64)
{
_scopes.Capacity = 64;
}
}

public void EnableParentNodeRecoding(ParserOptions options)
{
if (!ReferenceEquals(options._onNode?.Target, this))
{
_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;
}
Expand All @@ -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<Scope> 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<CatchClause>();

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<IFunction>();
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<IClass>()?.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);
}
}
}
Loading

0 comments on commit e1004c6

Please sign in to comment.