Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convenience API for dealing with variable scopes #13

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
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: default);

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
Loading