diff --git a/src/Esprima.SourceGenerators/Esprima.SourceGenerators.csproj b/src/Esprima.SourceGenerators/Esprima.SourceGenerators.csproj index 5dfd28a0..5be2b0b2 100644 --- a/src/Esprima.SourceGenerators/Esprima.SourceGenerators.csproj +++ b/src/Esprima.SourceGenerators/Esprima.SourceGenerators.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Esprima/Ast/ArrayExpression.cs b/src/Esprima/Ast/ArrayExpression.cs index 39a22c49..2f8055c8 100644 --- a/src/Esprima/Ast/ArrayExpression.cs +++ b/src/Esprima/Ast/ArrayExpression.cs @@ -12,6 +12,9 @@ public ArrayExpression(in NodeList elements) : base(Nodes.ArrayExpr _elements = elements; } + /// + /// { (incl. ) | } + /// public ref readonly NodeList Elements { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref _elements; } internal override Node? NextChildNode(ref ChildNodes.Enumerator enumerator) => enumerator.MoveNextNullable(Elements); diff --git a/src/Esprima/Ast/ArrayPattern.cs b/src/Esprima/Ast/ArrayPattern.cs index 2726f3dc..ed6f5535 100644 --- a/src/Esprima/Ast/ArrayPattern.cs +++ b/src/Esprima/Ast/ArrayPattern.cs @@ -12,6 +12,9 @@ public ArrayPattern(in NodeList elements) : base(Nodes.ArrayPattern _elements = elements; } + /// + /// { | | | | } + /// public ref readonly NodeList Elements { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref _elements; } internal override Node? NextChildNode(ref ChildNodes.Enumerator enumerator) => enumerator.MoveNextNullable(Elements); diff --git a/src/Esprima/Ast/ArrowFunctionExpression.cs b/src/Esprima/Ast/ArrowFunctionExpression.cs index f0bd0073..02272851 100644 --- a/src/Esprima/Ast/ArrowFunctionExpression.cs +++ b/src/Esprima/Ast/ArrowFunctionExpression.cs @@ -23,9 +23,12 @@ public ArrowFunctionExpression( } Identifier? IFunction.Id => null; + /// + /// { | | | } + /// public ref readonly NodeList Params { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref _params; } /// - /// | + /// | /// public Node Body { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } bool IFunction.Generator => false; diff --git a/src/Esprima/Ast/AssignmentExpression.cs b/src/Esprima/Ast/AssignmentExpression.cs index bbde336c..2024ed2e 100644 --- a/src/Esprima/Ast/AssignmentExpression.cs +++ b/src/Esprima/Ast/AssignmentExpression.cs @@ -94,8 +94,9 @@ public static string GetAssignmentOperatorToken(AssignmentOperator op) } public AssignmentOperator Operator { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } + /// - /// Can be something else than Expression (, ) in case of destructuring assignment + /// | /// public Expression Left { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public Expression Right { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/AssignmentPattern.cs b/src/Esprima/Ast/AssignmentPattern.cs index ea9b4d95..16a61d72 100644 --- a/src/Esprima/Ast/AssignmentPattern.cs +++ b/src/Esprima/Ast/AssignmentPattern.cs @@ -13,6 +13,9 @@ public AssignmentPattern(Expression left, Expression right) : base(Nodes.Assignm _right = right; } + /// + /// | + /// public Expression Left { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public Expression Right { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _right; } diff --git a/src/Esprima/Ast/CatchClause.cs b/src/Esprima/Ast/CatchClause.cs index 5a17b185..7037dfc2 100644 --- a/src/Esprima/Ast/CatchClause.cs +++ b/src/Esprima/Ast/CatchClause.cs @@ -13,7 +13,7 @@ public CatchClause(Expression? param, BlockStatement body) : } /// - /// BindingIdentifier | | + /// | /// public Expression? Param { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public BlockStatement Body { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/ChainExpression.cs b/src/Esprima/Ast/ChainExpression.cs index 0b8ec339..79615016 100644 --- a/src/Esprima/Ast/ChainExpression.cs +++ b/src/Esprima/Ast/ChainExpression.cs @@ -11,7 +11,7 @@ public ChainExpression(Expression expression) : base(Nodes.ChainExpression) } /// - /// | | + /// | | /// public Expression Expression { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/ClassBody.cs b/src/Esprima/Ast/ClassBody.cs index cd435a86..b5a857f8 100644 --- a/src/Esprima/Ast/ClassBody.cs +++ b/src/Esprima/Ast/ClassBody.cs @@ -13,7 +13,7 @@ public ClassBody(in NodeList body) : base(Nodes.ClassBody) } /// - /// | | + /// | | /// public ref readonly NodeList Body { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref _body; } diff --git a/src/Esprima/Ast/ClassDeclaration.cs b/src/Esprima/Ast/ClassDeclaration.cs index a9691e5a..7a15b29e 100644 --- a/src/Esprima/Ast/ClassDeclaration.cs +++ b/src/Esprima/Ast/ClassDeclaration.cs @@ -18,7 +18,7 @@ public ClassDeclaration(Identifier? id, Expression? superClass, ClassBody body, public Identifier? Id { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } /// - /// | + /// | /// public Expression? SuperClass { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public ClassBody Body { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/ClassExpression.cs b/src/Esprima/Ast/ClassExpression.cs index e163ee6d..dbfb34b2 100644 --- a/src/Esprima/Ast/ClassExpression.cs +++ b/src/Esprima/Ast/ClassExpression.cs @@ -21,7 +21,7 @@ public ClassExpression( public Identifier? Id { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } /// - /// | + /// | /// public Expression? SuperClass { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public ClassBody Body { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/ClassProperty.cs b/src/Esprima/Ast/ClassProperty.cs index 3b58eea2..567895b8 100644 --- a/src/Esprima/Ast/ClassProperty.cs +++ b/src/Esprima/Ast/ClassProperty.cs @@ -13,7 +13,7 @@ protected ClassProperty(Nodes type, PropertyKind kind, Expression key, bool comp public PropertyKind Kind { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } /// - /// | | '[' ']' + /// | (string or numeric) | '[' ']' | /// public Expression Key { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public bool Computed { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/ExportAllDeclaration.cs b/src/Esprima/Ast/ExportAllDeclaration.cs index 49119e88..d14d6e07 100644 --- a/src/Esprima/Ast/ExportAllDeclaration.cs +++ b/src/Esprima/Ast/ExportAllDeclaration.cs @@ -20,7 +20,7 @@ public ExportAllDeclaration(Literal source, Expression? exported, in NodeList - /// | StringLiteral () + /// | (string) /// public Expression? Exported { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public ref readonly NodeList Assertions { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref _assertions; } diff --git a/src/Esprima/Ast/ExportDefaultDeclaration.cs b/src/Esprima/Ast/ExportDefaultDeclaration.cs index 8ecdfe8d..2c2c3588 100644 --- a/src/Esprima/Ast/ExportDefaultDeclaration.cs +++ b/src/Esprima/Ast/ExportDefaultDeclaration.cs @@ -11,7 +11,7 @@ public ExportDefaultDeclaration(StatementListItem declaration) : base(Nodes.Expo } /// - /// BindingIdentifier | | | | + /// | | /// public StatementListItem Declaration { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/ExportNamedDeclaration.cs b/src/Esprima/Ast/ExportNamedDeclaration.cs index fb7d4bd9..ebccf22b 100644 --- a/src/Esprima/Ast/ExportNamedDeclaration.cs +++ b/src/Esprima/Ast/ExportNamedDeclaration.cs @@ -21,6 +21,9 @@ public ExportNamedDeclaration( _assertions = assertions; } + /// + /// | | + /// public StatementListItem? Declaration { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public ref readonly NodeList Specifiers { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref _specifiers; } public Literal? Source { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/ExportSpecifier.cs b/src/Esprima/Ast/ExportSpecifier.cs index 52d01c38..19bf0af1 100644 --- a/src/Esprima/Ast/ExportSpecifier.cs +++ b/src/Esprima/Ast/ExportSpecifier.cs @@ -12,11 +12,11 @@ public ExportSpecifier(Expression local, Expression exported) : base(Nodes.Expor } /// - /// | StringLiteral () + /// | (string) /// public Expression Local { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } /// - /// | + /// | (string) /// public Expression Exported { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/ForInStatement.cs b/src/Esprima/Ast/ForInStatement.cs index 241c1563..9afcc487 100644 --- a/src/Esprima/Ast/ForInStatement.cs +++ b/src/Esprima/Ast/ForInStatement.cs @@ -15,6 +15,9 @@ public ForInStatement( Body = body; } + /// + /// (may have an initializer in non-strict mode) | | + /// public Node Left { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public Expression Right { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public Statement Body { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/ForOfStatement.cs b/src/Esprima/Ast/ForOfStatement.cs index 5723a7c6..45944e9a 100644 --- a/src/Esprima/Ast/ForOfStatement.cs +++ b/src/Esprima/Ast/ForOfStatement.cs @@ -17,6 +17,9 @@ public ForOfStatement( Await = await; } + /// + /// (cannot have an initializer) | | + /// public Node Left { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public Expression Right { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public Statement Body { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/ForStatement.cs b/src/Esprima/Ast/ForStatement.cs index 46bf441f..6872cd58 100644 --- a/src/Esprima/Ast/ForStatement.cs +++ b/src/Esprima/Ast/ForStatement.cs @@ -19,7 +19,7 @@ public ForStatement( } /// - /// (var i) | (i=0) + /// (var i) | (i=0) /// public StatementListItem? Init { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public Expression? Test { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/FunctionDeclaration.cs b/src/Esprima/Ast/FunctionDeclaration.cs index 3bc0eb11..ad69a8ea 100644 --- a/src/Esprima/Ast/FunctionDeclaration.cs +++ b/src/Esprima/Ast/FunctionDeclaration.cs @@ -25,6 +25,9 @@ public FunctionDeclaration( } public Identifier? Id { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } + /// + /// { | | | } + /// public ref readonly NodeList Params { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref _params; } public BlockStatement Body { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/FunctionExpression.cs b/src/Esprima/Ast/FunctionExpression.cs index 4b1abf41..703532e4 100644 --- a/src/Esprima/Ast/FunctionExpression.cs +++ b/src/Esprima/Ast/FunctionExpression.cs @@ -25,6 +25,9 @@ public FunctionExpression( } public Identifier? Id { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } + /// + /// { | | | } + /// public ref readonly NodeList Params { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref _params; } public BlockStatement Body { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/IClass.cs b/src/Esprima/Ast/IClass.cs index 453b64d7..cecda0e8 100644 --- a/src/Esprima/Ast/IClass.cs +++ b/src/Esprima/Ast/IClass.cs @@ -5,6 +5,7 @@ /// public interface IClass { + Nodes Type { get; } Identifier? Id { get; } Expression? SuperClass { get; } ClassBody Body { get; } diff --git a/src/Esprima/Ast/IFunction.cs b/src/Esprima/Ast/IFunction.cs index 88d99920..4ecf3271 100644 --- a/src/Esprima/Ast/IFunction.cs +++ b/src/Esprima/Ast/IFunction.cs @@ -5,6 +5,7 @@ /// public interface IFunction { + Nodes Type { get; } Identifier? Id { get; } ref readonly NodeList Params { get; } Node Body { get; } diff --git a/src/Esprima/Ast/IProperty.cs b/src/Esprima/Ast/IProperty.cs index 9b9b4811..0fdecd41 100644 --- a/src/Esprima/Ast/IProperty.cs +++ b/src/Esprima/Ast/IProperty.cs @@ -2,6 +2,7 @@ { public interface IProperty { + Nodes Type { get; } PropertyKind Kind { get; } Expression Key { get; } bool Computed { get; } diff --git a/src/Esprima/Ast/Identifier.cs b/src/Esprima/Ast/Identifier.cs index 8d654f36..56eabb5d 100644 --- a/src/Esprima/Ast/Identifier.cs +++ b/src/Esprima/Ast/Identifier.cs @@ -1,10 +1,8 @@ -using System.Diagnostics; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using Esprima.Utils; namespace Esprima.Ast { - [DebuggerDisplay("{Name,nq}")] public sealed class Identifier : Expression { public Identifier(string? name) : base(Nodes.Identifier) diff --git a/src/Esprima/Ast/ImportAttribute.cs b/src/Esprima/Ast/ImportAttribute.cs index 30e41b7a..95956995 100644 --- a/src/Esprima/Ast/ImportAttribute.cs +++ b/src/Esprima/Ast/ImportAttribute.cs @@ -12,7 +12,7 @@ public ImportAttribute(Expression key, Literal value) : base(Nodes.ImportAttribu } /// - /// | + /// | /// public Expression Key { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public Literal Value { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/ImportSpecifier.cs b/src/Esprima/Ast/ImportSpecifier.cs index 91cfd18e..3e1f40e9 100644 --- a/src/Esprima/Ast/ImportSpecifier.cs +++ b/src/Esprima/Ast/ImportSpecifier.cs @@ -11,7 +11,7 @@ public ImportSpecifier(Identifier local, Expression imported) : base(local, Node } /// - /// | StringLiteral () + /// | (string) /// public Expression Imported { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/Literal.cs b/src/Esprima/Ast/Literal.cs index 271d31fe..99d083fc 100644 --- a/src/Esprima/Ast/Literal.cs +++ b/src/Esprima/Ast/Literal.cs @@ -1,12 +1,10 @@ -using System.Diagnostics; -using System.Numerics; +using System.Numerics; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using Esprima.Utils; namespace Esprima.Ast { - [DebuggerDisplay("{Raw,nq}")] public sealed class Literal : Expression { internal Literal(TokenType tokenType, object? value, string raw) : base(Nodes.Literal) diff --git a/src/Esprima/Ast/Node.cs b/src/Esprima/Ast/Node.cs index 020eee43..2a5d7290 100644 --- a/src/Esprima/Ast/Node.cs +++ b/src/Esprima/Ast/Node.cs @@ -1,8 +1,10 @@ -using System.Runtime.CompilerServices; +using System.Diagnostics; +using System.Runtime.CompilerServices; using Esprima.Utils; namespace Esprima.Ast { + [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(), nq}}")] public abstract class Node { protected Node(Nodes type) @@ -43,5 +45,12 @@ protected Node(Nodes type) { return visitor.VisitExtension(this); } + + public override string ToString() => this.ToJavascriptString(beautify: true); + + private string GetDebuggerDisplay() + { + return $"/*{Type}*/ {this}"; + } } } diff --git a/src/Esprima/Ast/NodeExtensions.cs b/src/Esprima/Ast/NodeExtensions.cs index 54287624..7f2eb388 100644 --- a/src/Esprima/Ast/NodeExtensions.cs +++ b/src/Esprima/Ast/NodeExtensions.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Runtime.CompilerServices; using static Esprima.EsprimaExceptionHelper; using NodeSysList = System.Collections.Generic.List; @@ -7,6 +8,7 @@ namespace Esprima.Ast public static class NodeExtensions { [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T As(this Node node) where T : Node { return (T) node; diff --git a/src/Esprima/Ast/NodeList.cs b/src/Esprima/Ast/NodeList.cs index 41f18801..33b6a2ac 100644 --- a/src/Esprima/Ast/NodeList.cs +++ b/src/Esprima/Ast/NodeList.cs @@ -32,11 +32,18 @@ public int Count get => _count; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public NodeList AsNodes() { return new NodeList(_items /* conversion by co-variance! */, _count); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public NodeList As() where TTo : Node? + { + return new NodeList((TTo[]?) (object?) _items, _count); + } + public T this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -203,6 +210,7 @@ var count } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool AreSame(in NodeList nodeList1, in NodeList nodeList2) where T : Node? { return nodeList1._items == nodeList2._items; diff --git a/src/Esprima/Ast/ObjectExpression.cs b/src/Esprima/Ast/ObjectExpression.cs index d838a9f4..e600e033 100644 --- a/src/Esprima/Ast/ObjectExpression.cs +++ b/src/Esprima/Ast/ObjectExpression.cs @@ -12,6 +12,9 @@ public ObjectExpression(in NodeList properties) : base(Nodes.ObjectE _properties = properties; } + /// + /// { | } + /// public ref readonly NodeList Properties { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref _properties; } internal override Node? NextChildNode(ref ChildNodes.Enumerator enumerator) => enumerator.MoveNext(Properties); diff --git a/src/Esprima/Ast/ObjectPattern.cs b/src/Esprima/Ast/ObjectPattern.cs index b8d3c5be..1025262e 100644 --- a/src/Esprima/Ast/ObjectPattern.cs +++ b/src/Esprima/Ast/ObjectPattern.cs @@ -12,6 +12,9 @@ public ObjectPattern(in NodeList properties) : base(Nodes.ObjectPattern) _properties = properties; } + /// + /// { | } + /// public ref readonly NodeList Properties { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref _properties; } internal override Node? NextChildNode(ref ChildNodes.Enumerator enumerator) => enumerator.MoveNext(Properties); diff --git a/src/Esprima/Ast/Property.cs b/src/Esprima/Ast/Property.cs index 51e5e77a..0a9ba311 100644 --- a/src/Esprima/Ast/Property.cs +++ b/src/Esprima/Ast/Property.cs @@ -26,11 +26,15 @@ public Property( public PropertyKind Kind { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } /// - /// | | '[' ']' + /// | (string or numeric) | '[' ']' /// public Expression Key { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public bool Computed { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } + /// + /// When property of an object literal: (incl. and for getters/setters/methods)
+ /// When property of an object binding pattern: | | | + ///
public Expression Value { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _value; } Expression? IProperty.Value => Value; diff --git a/src/Esprima/Ast/RestElement.cs b/src/Esprima/Ast/RestElement.cs index bfc525bc..36604083 100644 --- a/src/Esprima/Ast/RestElement.cs +++ b/src/Esprima/Ast/RestElement.cs @@ -11,7 +11,7 @@ public RestElement(Expression argument) : base(Nodes.RestElement) } /// - /// BindingIdentifier | + /// | /// public Expression Argument { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Ast/VariableDeclaration.cs b/src/Esprima/Ast/VariableDeclaration.cs index 06c24b87..e8b40799 100644 --- a/src/Esprima/Ast/VariableDeclaration.cs +++ b/src/Esprima/Ast/VariableDeclaration.cs @@ -1,10 +1,22 @@ using System.Runtime.CompilerServices; using Esprima.Utils; +using static Esprima.EsprimaExceptionHelper; namespace Esprima.Ast { public sealed class VariableDeclaration : Declaration { + public static string GetVariableDeclarationKindToken(VariableDeclarationKind kind) + { + return kind switch + { + VariableDeclarationKind.Var => "var", + VariableDeclarationKind.Let => "let", + VariableDeclarationKind.Const => "const", + _ => ThrowArgumentOutOfRangeException(nameof(kind), "Invalid variable declaration kind: " + kind) + }; + } + private readonly NodeList _declarations; public VariableDeclaration( diff --git a/src/Esprima/Ast/VariableDeclarator.cs b/src/Esprima/Ast/VariableDeclarator.cs index ad66128d..4df78d07 100644 --- a/src/Esprima/Ast/VariableDeclarator.cs +++ b/src/Esprima/Ast/VariableDeclarator.cs @@ -13,7 +13,7 @@ public VariableDeclarator(Expression id, Expression? init) : } /// - /// BindingIdentifier | + /// | /// public Expression Id { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } public Expression? Init { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } diff --git a/src/Esprima/Esprima.csproj b/src/Esprima/Esprima.csproj index 19e4d566..00461454 100644 --- a/src/Esprima/Esprima.csproj +++ b/src/Esprima/Esprima.csproj @@ -37,6 +37,7 @@ + diff --git a/src/Esprima/EsprimaExceptionHelper.cs b/src/Esprima/EsprimaExceptionHelper.cs index 7f1f1164..c0abe03d 100644 --- a/src/Esprima/EsprimaExceptionHelper.cs +++ b/src/Esprima/EsprimaExceptionHelper.cs @@ -1,37 +1,49 @@ -namespace Esprima +using System.Diagnostics.CodeAnalysis; + +namespace Esprima { + /// + /// JIT cannot inline methods that have in them. These helper methods allow us to work around this. + /// internal static class EsprimaExceptionHelper { + [DoesNotReturn] public static void ThrowIndexOutOfRangeException() { throw new IndexOutOfRangeException(); } + [DoesNotReturn] public static T ThrowArgumentOutOfRangeException(string paramName, object actualValue, string? message = null) { throw new ArgumentOutOfRangeException(paramName, actualValue, message); } + [DoesNotReturn] public static void ThrowArgumentOutOfRangeException(string paramName, object actualValue, string? message = null) { throw new ArgumentOutOfRangeException(paramName, actualValue, message); } + [DoesNotReturn] public static T ThrowInvalidOperationException(string? message = null) { throw new InvalidOperationException(message); } + [DoesNotReturn] public static void ThrowInvalidOperationException(string? message = null) { throw new InvalidOperationException(message); } + [DoesNotReturn] public static void ThrowArgumentNullException(string message) { throw new ArgumentNullException(message); } + [DoesNotReturn] public static T ThrowArgumentNullException(string message) { throw new ArgumentNullException(message); diff --git a/src/Esprima/Scanner.cs b/src/Esprima/Scanner.cs index 3b8e32f8..0e45c5e9 100644 --- a/src/Esprima/Scanner.cs +++ b/src/Esprima/Scanner.cs @@ -1800,7 +1800,7 @@ public Regex ParseRegex(string pattern, string flags, TimeSpan matchTimeout) var index = 0; var newPattern = tmp; - if (options.HasFlag(RegexOptions.Multiline)) + if ((options & RegexOptions.Multiline) == RegexOptions.Multiline) { while ((index = newPattern.IndexOf("$", index, StringComparison.Ordinal)) != -1) { diff --git a/src/Esprima/Utils/AstJson.cs b/src/Esprima/Utils/AstJson.cs deleted file mode 100644 index c9ad5562..00000000 --- a/src/Esprima/Utils/AstJson.cs +++ /dev/null @@ -1,1295 +0,0 @@ -using System.Collections; -using System.Globalization; -using System.Numerics; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text.RegularExpressions; -using Esprima.Ast; -using static Esprima.EsprimaExceptionHelper; - -namespace Esprima.Utils; - -public enum LocationMembersPlacement -{ - End, - Start -} - -public static class AstJson -{ - public sealed class Options - { - public static readonly Options Default = new(); - - public bool IncludingLineColumn { get; private set; } - public bool IncludingRange { get; private set; } - public LocationMembersPlacement LocationMembersPlacement { get; private set; } - /// - /// This switch is intended for enabling a compatibility mode for to build a JSON output - /// which matches the format of the test fixtures of the original Esprima project. - /// - internal bool TestCompatibilityMode { get; private set; } - - public Options() { } - - private Options(Options options) - { - IncludingLineColumn = options.IncludingLineColumn; - IncludingRange = options.IncludingRange; - LocationMembersPlacement = options.LocationMembersPlacement; - } - - public Options WithIncludingLineColumn(bool value) - { - return value == IncludingLineColumn ? this : new Options(this) { IncludingLineColumn = value }; - } - - public Options WithIncludingRange(bool value) - { - return value == IncludingRange ? this : new Options(this) { IncludingRange = value }; - } - - public Options WithLocationMembersPlacement(LocationMembersPlacement value) - { - return value == LocationMembersPlacement ? this : new Options(this) { LocationMembersPlacement = value }; - } - - internal Options WithTestCompatibilityMode(bool value) - { - return value == TestCompatibilityMode ? this : new Options(this) { TestCompatibilityMode = value }; - } - } - - public interface IConverter - { - void WriteJson(Node node, JsonWriter writer, Options options); - } - - public static string ToJsonString(this Node node, IConverter? converter = null) - { - return ToJsonString(node, indent: null, converter); - } - - public static string ToJsonString(this Node node, string? indent, IConverter? converter = null) - { - return ToJsonString(node, Options.Default, indent, converter); - } - - public static string ToJsonString(this Node node, Options options, IConverter? converter = null) - { - return ToJsonString(node, options, null, converter); - } - - public static string ToJsonString(this Node node, Options options, string? indent, IConverter? converter = null) - { - using (var writer = new StringWriter()) - { - WriteJson(node, writer, options, indent, converter); - return writer.ToString(); - } - } - - public static void WriteJson(this Node node, TextWriter writer, IConverter? converter = null) - { - WriteJson(node, writer, indent: null, converter); - } - - public static void WriteJson(this Node node, TextWriter writer, string? indent, IConverter? converter = null) - { - WriteJson(node, writer, Options.Default, indent, converter); - } - - public static void WriteJson(this Node node, TextWriter writer, Options options, IConverter? converter = null) - { - WriteJson(node, writer, options, null, converter); - } - - public static void WriteJson(this Node node, TextWriter writer, Options options, string? indent, IConverter? converter = null) - { - if (node == null) - { - ThrowArgumentNullException(nameof(node)); - return; - } - - if (writer == null) - { - ThrowArgumentNullException(nameof(writer)); - return; - } - - if (options == null) - { - ThrowArgumentNullException(nameof(options)); - return; - } - - (converter ?? AstToJsonConverter.Default).WriteJson(node, new JsonTextWriter(writer, indent), options); - } - - public static void WriteJson(this Node node, JsonWriter writer, Options options, IConverter? converter = null) - { - if (node == null) - { - ThrowArgumentNullException(nameof(node)); - return; - } - - if (writer == null) - { - ThrowArgumentNullException(nameof(writer)); - return; - } - - if (options == null) - { - ThrowArgumentNullException(nameof(options)); - return; - } - - (converter ?? AstToJsonConverter.Default).WriteJson(node, writer, options); - } -} - -public class AstToJsonConverter : AstJson.IConverter -{ - public static readonly AstToJsonConverter Default = new(); - - private protected AstToJsonConverter() { } - - private protected virtual VisitorBase CreateVisitor(JsonWriter writer, AstJson.Options options) - { - return new Visitor(writer, options); - } - - public void WriteJson(Node node, JsonWriter writer, AstJson.Options options) - { - CreateVisitor(writer, options).Visit(node); - } - - private protected abstract class VisitorBase : AstVisitor - { - private readonly JsonWriter _writer; - private protected readonly bool _includeLineColumn; - private protected readonly bool _includeRange; - private protected readonly LocationMembersPlacement _locationMembersPlacement; - private protected readonly bool _testCompatibilityMode; - - public VisitorBase(JsonWriter writer, AstJson.Options options) - { - _writer = writer ?? ThrowArgumentNullException(nameof(writer)); - - _includeLineColumn = options.IncludingLineColumn; - _includeRange = options.IncludingRange; - _locationMembersPlacement = options.LocationMembersPlacement; - _testCompatibilityMode = options.TestCompatibilityMode; - } - - protected virtual string GetNodeType(Node node) - { - return node.Type.ToString(); - } - - private void WriteLocationInfo(Node node) - { - if (node is ChainExpression) - { - return; - } - - if (_includeRange) - { - _writer.Member("range"); - _writer.StartArray(); - _writer.Number(node.Range.Start); - _writer.Number(node.Range.End); - _writer.EndArray(); - } - - if (_includeLineColumn) - { - _writer.Member("loc"); - _writer.StartObject(); - _writer.Member("start"); - Write(node.Location.Start); - _writer.Member("end"); - Write(node.Location.End); - _writer.EndObject(); - } - - void Write(Position position) - { - _writer.StartObject(); - Member("line", position.Line); - Member("column", position.Column); - _writer.EndObject(); - } - } - - private void OnStartNodeObject(Node node) - { - _writer.StartObject(); - - if ((_includeLineColumn || _includeRange) - && _locationMembersPlacement == LocationMembersPlacement.Start) - { - WriteLocationInfo(node); - } - - Member("type", GetNodeType(node)); - } - - private void OnFinishNodeObject(Node node) - { - if ((_includeLineColumn || _includeRange) - && _locationMembersPlacement == LocationMembersPlacement.End) - { - WriteLocationInfo(node); - } - - _writer.EndObject(); - } - - protected readonly struct NodeObjectDisposable : IDisposable - { - private readonly VisitorBase _visitor; - private readonly Node _node; - - public NodeObjectDisposable(VisitorBase visitor, Node node) - { - _visitor = visitor; - _node = node; - } - - public void Dispose() - { - _visitor.OnFinishNodeObject(_node); - } - } - - protected NodeObjectDisposable StartNodeObject(Node node) - { - OnStartNodeObject(node); - return new NodeObjectDisposable(this, node); - } - - protected void EmptyNodeObject(Node node) - { - using (StartNodeObject(node)) { } - } - - protected void Member(string name) - { - _writer.Member(name); - } - - protected void Member(string name, Node? node) - { - Member(name); - Visit(node); - } - - protected void Member(string name, string? value) - { - Member(name); - _writer.String(value); - } - - protected void Member(string name, bool value) - { - Member(name); - _writer.Boolean(value); - } - - protected void Member(string name, int value) - { - Member(name); - _writer.Number(value); - } - - private static readonly ConditionalWeakTable EnumMap = new(); - - protected void Member(string name, T value) where T : Enum - { - var map = (Dictionary) - EnumMap.GetValue(value.GetType(), - t => t.GetRuntimeFields() - .Where(f => f.IsStatic) - .ToDictionary(f => (T) f.GetValue(null), f => f.Name.ToLowerInvariant())); - Member(name, map[value]); - } - - protected void Member(string name, in NodeList nodes) where T : Node? - { - Member(name, nodes, node => node); - } - - protected void Member(string name, in NodeList list, Func nodeSelector) where T : Node? - { - Member(name); - _writer.StartArray(); - foreach (var item in list) - { - Visit(nodeSelector(item)); - } - - _writer.EndArray(); - } - - public override object? Visit(Node? node) - { - if (node is not null) - { - return base.Visit(node); - } - else - { - _writer.Null(); - return node!; - } - } - - protected internal override object? VisitArrayExpression(ArrayExpression arrayExpression) - { - using (StartNodeObject(arrayExpression)) - { - Member("elements", arrayExpression.Elements); - } - - return arrayExpression; - } - - protected internal override object? VisitArrayPattern(ArrayPattern arrayPattern) - { - using (StartNodeObject(arrayPattern)) - { - Member("elements", arrayPattern.Elements); - } - - return arrayPattern; - } - - protected internal override object? VisitArrowFunctionExpression(ArrowFunctionExpression arrowFunctionExpression) - { - using (StartNodeObject(arrowFunctionExpression)) - { - Member("id", ((IFunction) arrowFunctionExpression).Id); - Member("params", arrowFunctionExpression.Params); - Member("body", arrowFunctionExpression.Body); - Member("generator", ((IFunction) arrowFunctionExpression).Generator); - Member("expression", arrowFunctionExpression.Expression); - // original Esprima doesn't include this information yet - if (!_testCompatibilityMode) - { - Member("strict", arrowFunctionExpression.Strict); - } - Member("async", arrowFunctionExpression.Async); - } - - return arrowFunctionExpression; - } - - protected internal override object? VisitAssignmentExpression(AssignmentExpression assignmentExpression) - { - using (StartNodeObject(assignmentExpression)) - { - Member("operator", AssignmentExpression.GetAssignmentOperatorToken(assignmentExpression.Operator)); - Member("left", assignmentExpression.Left); - Member("right", assignmentExpression.Right); - } - - return assignmentExpression; - } - - protected internal override object? VisitAssignmentPattern(AssignmentPattern assignmentPattern) - { - using (StartNodeObject(assignmentPattern)) - { - Member("left", assignmentPattern.Left); - Member("right", assignmentPattern.Right); - } - - return assignmentPattern; - } - - protected internal override object? VisitAwaitExpression(AwaitExpression awaitExpression) - { - using (StartNodeObject(awaitExpression)) - { - Member("argument", awaitExpression.Argument); - } - - return awaitExpression; - } - - protected internal override object? VisitBinaryExpression(BinaryExpression binaryExpression) - { - using (StartNodeObject(binaryExpression)) - { - Member("operator", BinaryExpression.GetBinaryOperatorToken(binaryExpression.Operator)); - Member("left", binaryExpression.Left); - Member("right", binaryExpression.Right); - } - - return binaryExpression; - } - - protected internal override object? VisitBlockStatement(BlockStatement blockStatement) - { - using (StartNodeObject(blockStatement)) - { - Member("body", blockStatement.Body, e => (Statement) e); - } - - return blockStatement; - } - - protected internal override object? VisitBreakStatement(BreakStatement breakStatement) - { - using (StartNodeObject(breakStatement)) - { - Member("label", breakStatement.Label); - } - - return breakStatement; - } - - protected internal override object? VisitCallExpression(CallExpression callExpression) - { - using (StartNodeObject(callExpression)) - { - Member("callee", callExpression.Callee); - Member("arguments", callExpression.Arguments, e => e); - Member("optional", callExpression.Optional); - } - - return callExpression; - } - - protected internal override object? VisitCatchClause(CatchClause catchClause) - { - using (StartNodeObject(catchClause)) - { - Member("param", catchClause.Param); - Member("body", catchClause.Body); - } - - return catchClause; - } - - protected internal override object? VisitChainExpression(ChainExpression chainExpression) - { - using (StartNodeObject(chainExpression)) - { - Member("expression", chainExpression.Expression); - } - - return chainExpression; - } - - protected internal override object? VisitClassBody(ClassBody classBody) - { - using (StartNodeObject(classBody)) - { - Member("body", classBody.Body); - } - - return classBody; - } - - protected internal override object? VisitClassDeclaration(ClassDeclaration classDeclaration) - { - using (StartNodeObject(classDeclaration)) - { - Member("id", classDeclaration.Id); - Member("superClass", classDeclaration.SuperClass); - Member("body", classDeclaration.Body); - if (classDeclaration.Decorators.Count > 0) - { - Member("decorators", classDeclaration.Decorators); - } - } - - return classDeclaration; - } - - protected internal override object? VisitClassExpression(ClassExpression classExpression) - { - using (StartNodeObject(classExpression)) - { - Member("id", classExpression.Id); - Member("superClass", classExpression.SuperClass); - Member("body", classExpression.Body); - if (classExpression.Decorators.Count > 0) - { - Member("decorators", classExpression.Decorators); - } - } - - return classExpression; - } - - protected internal override object? VisitConditionalExpression(ConditionalExpression conditionalExpression) - { - using (StartNodeObject(conditionalExpression)) - { - Member("test", conditionalExpression.Test); - Member("consequent", conditionalExpression.Consequent); - Member("alternate", conditionalExpression.Alternate); - } - - return conditionalExpression; - } - - protected internal override object? VisitContinueStatement(ContinueStatement continueStatement) - { - using (StartNodeObject(continueStatement)) - { - Member("label", continueStatement.Label); - } - - return continueStatement; - } - - protected internal override object? VisitDebuggerStatement(DebuggerStatement debuggerStatement) - { - EmptyNodeObject(debuggerStatement); - return debuggerStatement; - } - - protected internal override object? VisitDecorator(Decorator decorator) - { - using (StartNodeObject(decorator)) - { - Member("expression", decorator.Expression); - } - - return decorator; - } - - protected internal override object? VisitDoWhileStatement(DoWhileStatement doWhileStatement) - { - using (StartNodeObject(doWhileStatement)) - { - Member("body", doWhileStatement.Body); - Member("test", doWhileStatement.Test); - } - - return doWhileStatement; - } - - protected internal override object? VisitEmptyStatement(EmptyStatement emptyStatement) - { - EmptyNodeObject(emptyStatement); - return emptyStatement; - } - - protected internal override object? VisitExportAllDeclaration(ExportAllDeclaration exportAllDeclaration) - { - using (StartNodeObject(exportAllDeclaration)) - { - Member("source", exportAllDeclaration.Source); - - // original Esprima doesn't include this information yet - if (!_testCompatibilityMode) - { - Member("exported", exportAllDeclaration.Exported); - if (exportAllDeclaration.Assertions.Count > 0) - { - Member("assertions", exportAllDeclaration.Assertions); - } - } - } - - return exportAllDeclaration; - } - - protected internal override object? VisitExportDefaultDeclaration(ExportDefaultDeclaration exportDefaultDeclaration) - { - using (StartNodeObject(exportDefaultDeclaration)) - { - Member("declaration", exportDefaultDeclaration.Declaration); - } - - return exportDefaultDeclaration; - } - - protected internal override object? VisitExportNamedDeclaration(ExportNamedDeclaration exportNamedDeclaration) - { - using (StartNodeObject(exportNamedDeclaration)) - { - Member("declaration", exportNamedDeclaration.Declaration); - Member("specifiers", exportNamedDeclaration.Specifiers); - Member("source", exportNamedDeclaration.Source); - // original Esprima doesn't include this information yet - if (!_testCompatibilityMode && exportNamedDeclaration.Assertions.Count > 0) - { - Member("assertions", exportNamedDeclaration.Assertions); - } - } - - return exportNamedDeclaration; - } - - protected internal override object? VisitExportSpecifier(ExportSpecifier exportSpecifier) - { - using (StartNodeObject(exportSpecifier)) - { - Member("exported", exportSpecifier.Exported); - Member("local", exportSpecifier.Local); - } - - return exportSpecifier; - } - - protected internal override object? VisitExpressionStatement(ExpressionStatement expressionStatement) - { - using (StartNodeObject(expressionStatement)) - { - if (expressionStatement is Directive d) - { - Member("directive", d.Directiv); - } - - Member("expression", expressionStatement.Expression); - } - - return expressionStatement; - } - - protected internal override object? VisitExtension(Node node) - { - throw new NotSupportedException("Unknown node type: " + node.Type); - } - - protected internal override object? VisitForInStatement(ForInStatement forInStatement) - { - using (StartNodeObject(forInStatement)) - { - Member("left", forInStatement.Left); - Member("right", forInStatement.Right); - Member("body", forInStatement.Body); - Member("each", false); - } - - return forInStatement; - } - - protected internal override object? VisitForOfStatement(ForOfStatement forOfStatement) - { - using (StartNodeObject(forOfStatement)) - { - Member("await", forOfStatement.Await); - Member("left", forOfStatement.Left); - Member("right", forOfStatement.Right); - Member("body", forOfStatement.Body); - } - - return forOfStatement; - } - - protected internal override object? VisitForStatement(ForStatement forStatement) - { - using (StartNodeObject(forStatement)) - { - Member("init", forStatement.Init); - Member("test", forStatement.Test); - Member("update", forStatement.Update); - Member("body", forStatement.Body); - } - - return forStatement; - } - - protected internal override object? VisitFunctionDeclaration(FunctionDeclaration functionDeclaration) - { - using (StartNodeObject(functionDeclaration)) - { - Member("id", functionDeclaration.Id); - Member("params", functionDeclaration.Params); - Member("body", functionDeclaration.Body); - Member("generator", functionDeclaration.Generator); - Member("expression", ((IFunction) functionDeclaration).Expression); - // original Esprima doesn't include this information yet - if (!_testCompatibilityMode) - { - Member("strict", functionDeclaration.Strict); - } - Member("async", functionDeclaration.Async); - } - - return functionDeclaration; - } - - protected internal override object? VisitFunctionExpression(FunctionExpression functionExpression) - { - using (StartNodeObject(functionExpression)) - { - Member("id", functionExpression.Id); - Member("params", functionExpression.Params); - Member("body", functionExpression.Body); - Member("generator", functionExpression.Generator); - Member("expression", ((IFunction) functionExpression).Expression); - // original Esprima doesn't include this information yet - if (!_testCompatibilityMode) - { - Member("strict", functionExpression.Strict); - } - Member("async", functionExpression.Async); - } - - return functionExpression; - } - - protected internal override object? VisitIdentifier(Identifier identifier) - { - using (StartNodeObject(identifier)) - { - Member("name", identifier.Name); - } - - return identifier; - } - - protected internal override object? VisitIfStatement(IfStatement ifStatement) - { - using (StartNodeObject(ifStatement)) - { - Member("test", ifStatement.Test); - Member("consequent", ifStatement.Consequent); - Member("alternate", ifStatement.Alternate); - } - - return ifStatement; - } - - private object? VisitImportCompat(ImportCompat import) - { - EmptyNodeObject(import); - return import; - } - - private sealed class ImportCompat : Expression - { - public ImportCompat() : base(Nodes.Import) { } - - internal override Node? NextChildNode(ref ChildNodes.Enumerator enumerator) => null; - - protected internal override object? Accept(AstVisitor visitor) => ((VisitorBase) visitor).VisitImportCompat(this); - } - - protected internal override object? VisitImport(Import import) - { - // original Esprima uses CallExpression to represent dynamic imports currently, - // so we need to rewrite our representation to match this expectation - if (_testCompatibilityMode) - { - const string importToken = "import"; - - var callee = new ImportCompat - { - Location = new Location(import.Location.Start, new Position(import.Location.Start.Line, import.Location.Start.Column + importToken.Length)), - Range = new Ast.Range(import.Range.Start, import.Range.Start + importToken.Length) - }; - var args = new NodeList(new Expression[] { import.Source }); - var callExpression = new CallExpression(callee, args, optional: false) - { - Location = import.Location, - Range = import.Range, - }; - - return Visit(callExpression); - } - - using (StartNodeObject(import)) - { - if (!_testCompatibilityMode) - { - Member("source", import.Source); - - if (import.Attributes is not null) - { - Member("attributes", import.Attributes); - } - } - } - - return import; - } - - protected internal override object? VisitImportAttribute(ImportAttribute importAttribute) - { - using (StartNodeObject(importAttribute)) - { - Member("key", importAttribute.Key); - Member("value", importAttribute.Value); - } - - return importAttribute; - } - - protected internal override object? VisitImportDeclaration(ImportDeclaration importDeclaration) - { - using (StartNodeObject(importDeclaration)) - { - Member("specifiers", importDeclaration.Specifiers, e => (Node) e); - Member("source", importDeclaration.Source); - // original Esprima doesn't include this information yet - if (importDeclaration.Assertions.Count > 0) - { - Member("assertions", importDeclaration.Assertions); - } - } - - return importDeclaration; - } - - protected internal override object? VisitImportDefaultSpecifier(ImportDefaultSpecifier importDefaultSpecifier) - { - using (StartNodeObject(importDefaultSpecifier)) - { - Member("local", importDefaultSpecifier.Local); - } - - return importDefaultSpecifier; - } - - protected internal override object? VisitImportNamespaceSpecifier(ImportNamespaceSpecifier importNamespaceSpecifier) - { - using (StartNodeObject(importNamespaceSpecifier)) - { - Member("local", importNamespaceSpecifier.Local); - } - - return importNamespaceSpecifier; - } - - protected internal override object? VisitImportSpecifier(ImportSpecifier importSpecifier) - { - using (StartNodeObject(importSpecifier)) - { - Member("local", importSpecifier.Local); - Member("imported", importSpecifier.Imported); - } - - return importSpecifier; - } - - protected internal override object? VisitLabeledStatement(LabeledStatement labeledStatement) - { - using (StartNodeObject(labeledStatement)) - { - Member("label", labeledStatement.Label); - Member("body", labeledStatement.Body); - } - - return labeledStatement; - } - - protected internal override object? VisitLiteral(Literal literal) - { - using (StartNodeObject(literal)) - { - _writer.Member("value"); - var value = literal.Value; - - switch (value) - { - case null: - if (!_testCompatibilityMode && literal.TokenType == TokenType.RegularExpression) - { - // This is how esprima.org actually renders regexes since it relies on Regex.toString - _writer.String(literal.Raw); - } - else - { - _writer.Null(); - } - - break; - case bool b: - _writer.Boolean(b); - break; - case Regex _: - _writer.StartObject(); - _writer.EndObject(); - break; - case double d: - _writer.Number(d); - break; - default: - _writer.String(Convert.ToString(value, CultureInfo.InvariantCulture)); - break; - } - - Member("raw", literal.Raw); - - if (literal.Regex != null) - { - _writer.Member("regex"); - _writer.StartObject(); - Member("pattern", literal.Regex.Pattern); - Member("flags", literal.Regex.Flags); - _writer.EndObject(); - } - else if (literal.Value is BigInteger bigInt) - { - Member("bigint", bigInt.ToString(CultureInfo.InvariantCulture)); - } - } - - return literal; - } - - protected internal override object? VisitMemberExpression(MemberExpression memberExpression) - { - using (StartNodeObject(memberExpression)) - { - Member("computed", memberExpression.Computed); - Member("object", memberExpression.Object); - Member("property", memberExpression.Property); - Member("optional", memberExpression.Optional); - } - - return memberExpression; - } - - protected internal override object? VisitMetaProperty(MetaProperty metaProperty) - { - using (StartNodeObject(metaProperty)) - { - Member("meta", metaProperty.Meta); - Member("property", metaProperty.Property); - } - - return metaProperty; - } - - protected internal override object? VisitMethodDefinition(MethodDefinition methodDefinition) - { - using (StartNodeObject(methodDefinition)) - { - Member("key", methodDefinition.Key); - Member("computed", methodDefinition.Computed); - Member("value", methodDefinition.Value); - Member("kind", methodDefinition.Kind); - Member("static", methodDefinition.Static); - if (methodDefinition.Decorators.Count > 0) - { - Member("decorators", methodDefinition.Decorators); - } - } - - return methodDefinition; - } - - protected internal override object? VisitNewExpression(NewExpression newExpression) - { - using (StartNodeObject(newExpression)) - { - Member("callee", newExpression.Callee); - Member("arguments", newExpression.Arguments, e => (Node) e); - } - - return newExpression; - } - - protected internal override object? VisitObjectExpression(ObjectExpression objectExpression) - { - using (StartNodeObject(objectExpression)) - { - Member("properties", objectExpression.Properties); - } - - return objectExpression; - } - - protected internal override object? VisitObjectPattern(ObjectPattern objectPattern) - { - using (StartNodeObject(objectPattern)) - { - Member("properties", objectPattern.Properties); - } - - return objectPattern; - } - - protected internal override object? VisitPrivateIdentifier(PrivateIdentifier privateIdentifier) - { - using (StartNodeObject(privateIdentifier)) - { - Member("name", privateIdentifier.Name); - } - - return privateIdentifier; - } - - protected internal override object? VisitProgram(Program program) - { - using (StartNodeObject(program)) - { - Member("body", program.Body, e => (Node) e); - Member("sourceType", program.SourceType); - - // original Esprima doesn't include this information yet - if (!_testCompatibilityMode && program is Script s) - { - Member("strict", s.Strict); - } - } - - return program; - } - - protected internal override object? VisitProperty(Property property) - { - using (StartNodeObject(property)) - { - Member("key", property.Key); - Member("computed", property.Computed); - Member("value", property.Value); - Member("kind", property.Kind); - Member("method", property.Method); - Member("shorthand", property.Shorthand); - } - - return property; - } - - protected internal override object? VisitPropertyDefinition(PropertyDefinition propertyDefinition) - { - using (StartNodeObject(propertyDefinition)) - { - Member("key", propertyDefinition.Key); - Member("computed", propertyDefinition.Computed); - Member("value", propertyDefinition.Value); - Member("kind", propertyDefinition.Kind); - Member("static", propertyDefinition.Static); - if (propertyDefinition.Decorators.Count > 0) - { - Member("decorators", propertyDefinition.Decorators); - } - } - - return propertyDefinition; - } - - protected internal override object? VisitRestElement(RestElement restElement) - { - using (StartNodeObject(restElement)) - { - Member("argument", restElement.Argument); - } - - return restElement; - } - - protected internal override object? VisitReturnStatement(ReturnStatement returnStatement) - { - using (StartNodeObject(returnStatement)) - { - Member("argument", returnStatement.Argument); - } - - return returnStatement; - } - - protected internal override object? VisitSequenceExpression(SequenceExpression sequenceExpression) - { - using (StartNodeObject(sequenceExpression)) - { - Member("expressions", sequenceExpression.Expressions); - } - - return sequenceExpression; - } - - protected internal override object? VisitSpreadElement(SpreadElement spreadElement) - { - using (StartNodeObject(spreadElement)) - { - Member("argument", spreadElement.Argument); - } - - return spreadElement; - } - - protected internal override object? VisitStaticBlock(StaticBlock staticBlock) - { - using (StartNodeObject(staticBlock)) - { - Member("body", staticBlock.Body, e => (Statement) e); - } - - return staticBlock; - } - - protected internal override object? VisitSuper(Super super) - { - EmptyNodeObject(super); - return super; - } - - protected internal override object? VisitSwitchCase(SwitchCase switchCase) - { - using (StartNodeObject(switchCase)) - { - Member("test", switchCase.Test); - Member("consequent", switchCase.Consequent, e => (Node) e); - } - - return switchCase; - } - - protected internal override object? VisitSwitchStatement(SwitchStatement switchStatement) - { - using (StartNodeObject(switchStatement)) - { - Member("discriminant", switchStatement.Discriminant); - Member("cases", switchStatement.Cases); - } - - return switchStatement; - } - - protected internal override object? VisitTaggedTemplateExpression(TaggedTemplateExpression taggedTemplateExpression) - { - using (StartNodeObject(taggedTemplateExpression)) - { - Member("tag", taggedTemplateExpression.Tag); - Member("quasi", taggedTemplateExpression.Quasi); - } - - return taggedTemplateExpression; - } - - protected internal override object? VisitTemplateElement(TemplateElement templateElement) - { - using (StartNodeObject(templateElement)) - { - _writer.Member("value"); - _writer.StartObject(); - Member("raw", templateElement.Value.Raw); - Member("cooked", templateElement.Value.Cooked); - _writer.EndObject(); - Member("tail", templateElement.Tail); - } - - return templateElement; - } - - protected internal override object? VisitTemplateLiteral(TemplateLiteral templateLiteral) - { - using (StartNodeObject(templateLiteral)) - { - Member("quasis", templateLiteral.Quasis); - Member("expressions", templateLiteral.Expressions); - } - - return templateLiteral; - } - - protected internal override object? VisitThisExpression(ThisExpression thisExpression) - { - EmptyNodeObject(thisExpression); - return thisExpression; - } - - protected internal override object? VisitThrowStatement(ThrowStatement throwStatement) - { - using (StartNodeObject(throwStatement)) - { - Member("argument", throwStatement.Argument); - } - - return throwStatement; - } - - protected internal override object? VisitTryStatement(TryStatement tryStatement) - { - using (StartNodeObject(tryStatement)) - { - Member("block", tryStatement.Block); - Member("handler", tryStatement.Handler); - Member("finalizer", tryStatement.Finalizer); - } - - return tryStatement; - } - - protected internal override object? VisitUnaryExpression(UnaryExpression unaryExpression) - { - using (StartNodeObject(unaryExpression)) - { - Member("operator", UnaryExpression.GetUnaryOperatorToken(unaryExpression.Operator)); - Member("argument", unaryExpression.Argument); - Member("prefix", unaryExpression.Prefix); - } - - return unaryExpression; - } - - protected internal override object? VisitVariableDeclaration(VariableDeclaration variableDeclaration) - { - using (StartNodeObject(variableDeclaration)) - { - Member("declarations", variableDeclaration.Declarations); - Member("kind", variableDeclaration.Kind); - } - - return variableDeclaration; - } - - protected internal override object? VisitVariableDeclarator(VariableDeclarator variableDeclarator) - { - using (StartNodeObject(variableDeclarator)) - { - Member("id", variableDeclarator.Id); - Member("init", variableDeclarator.Init); - } - - return variableDeclarator; - } - - protected internal override object? VisitWhileStatement(WhileStatement whileStatement) - { - using (StartNodeObject(whileStatement)) - { - Member("test", whileStatement.Test); - Member("body", whileStatement.Body); - } - - return whileStatement; - } - - protected internal override object? VisitWithStatement(WithStatement withStatement) - { - using (StartNodeObject(withStatement)) - { - Member("object", withStatement.Object); - Member("body", withStatement.Body); - } - - return withStatement; - } - - protected internal override object? VisitYieldExpression(YieldExpression yieldExpression) - { - using (StartNodeObject(yieldExpression)) - { - Member("argument", yieldExpression.Argument); - Member("delegate", yieldExpression.Delegate); - } - - return yieldExpression; - } - } - - private sealed class Visitor : VisitorBase - { - public Visitor(JsonWriter writer, AstJson.Options options) - : base(writer, options) - { - } - } -} diff --git a/src/Esprima/Utils/AstToJavascript.cs b/src/Esprima/Utils/AstToJavascript.cs new file mode 100644 index 00000000..e36e0d1f --- /dev/null +++ b/src/Esprima/Utils/AstToJavascript.cs @@ -0,0 +1,72 @@ +using Esprima.Ast; + +namespace Esprima.Utils; + +public record class AstToJavascriptOptions +{ + public static readonly AstToJavascriptOptions Default = new(); + + protected internal virtual AstToJavascriptConverter CreateConverter(JavascriptTextWriter writer) => new AstToJavascriptConverter(writer, this); +} + +public static class AstToJavascript +{ + public static string ToJavascriptString(this Node node) + { + return ToJavascriptString(node, JavascriptTextWriterOptions.Default, AstToJavascriptOptions.Default); + } + + public static string ToJavascriptString(this Node node, KnRJavascriptTextWriterOptions formattingOptions) + { + return ToJavascriptString(node, formattingOptions, AstToJavascriptOptions.Default); + } + + public static string ToJavascriptString(this Node node, bool beautify) + { + return ToJavascriptString(node, beautify ? KnRJavascriptTextWriterOptions.Default : JavascriptTextWriterOptions.Default, AstToJavascriptOptions.Default); + } + + public static string ToJavascriptString(this Node node, JavascriptTextWriterOptions writerOptions, AstToJavascriptOptions options) + { + using (var writer = new StringWriter()) + { + WriteJavascript(node, writer, writerOptions, options); + return writer.ToString(); + } + } + + public static void WriteJavascript(this Node node, TextWriter writer) + { + WriteJavascript(node, writer, JavascriptTextWriterOptions.Default, AstToJavascriptOptions.Default); + } + + public static void WriteJavascript(this Node node, TextWriter writer, KnRJavascriptTextWriterOptions formattingOptions) + { + WriteJavascript(node, writer, formattingOptions, AstToJavascriptOptions.Default); + } + + public static void WriteJavascript(this Node node, TextWriter writer, bool beautify) + { + WriteJavascript(node, writer, beautify ? KnRJavascriptTextWriterOptions.Default : JavascriptTextWriterOptions.Default, AstToJavascriptOptions.Default); + } + + public static void WriteJavascript(this Node node, TextWriter writer, JavascriptTextWriterOptions writerOptions, AstToJavascriptOptions options) + { + if (writerOptions is null) + { + throw new ArgumentNullException(nameof(writerOptions)); + } + + WriteJavascript(node, writerOptions.CreateWriter(writer), options); + } + + public static void WriteJavascript(this Node node, JavascriptTextWriter writer, AstToJavascriptOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.CreateConverter(writer).Convert(node); + } +} diff --git a/src/Esprima/Utils/AstToJavascriptConverter.Enums.cs b/src/Esprima/Utils/AstToJavascriptConverter.Enums.cs new file mode 100644 index 00000000..11133e24 --- /dev/null +++ b/src/Esprima/Utils/AstToJavascriptConverter.Enums.cs @@ -0,0 +1,57 @@ +namespace Esprima.Utils; + +partial class AstToJavascriptConverter +{ + [Flags] + protected internal enum BinaryOperationFlags + { + None = 0, + LeftOperandNeedsBrackets = 1 << 0, + RightOperandNeedsBrackets = 1 << 1, + BothOperandsNeedBrackets = LeftOperandNeedsBrackets | RightOperandNeedsBrackets + } + + [Flags] + protected internal enum StatementFlags + { + None = 0, + + NeedsSemicolon = JavascriptTextWriter.StatementFlags.NeedsSemicolon, + MayOmitRightMostSemicolon = JavascriptTextWriter.StatementFlags.MayOmitRightMostSemicolon, + IsRightMost = JavascriptTextWriter.StatementFlags.IsRightMost, + IsStatementBody = JavascriptTextWriter.StatementFlags.IsStatementBody, + + NestedVariableDeclaration = 1 << 16, + } + + [Flags] + protected internal enum ExpressionFlags + { + None = 0, + + NeedsBrackets = JavascriptTextWriter.ExpressionFlags.NeedsBrackets, + IsLeftMost = JavascriptTextWriter.ExpressionFlags.IsLeftMost, + + SpaceBeforeBracketsRecommended = JavascriptTextWriter.ExpressionFlags.SpaceBeforeBracketsRecommended, + SpaceAfterBracketsRecommended = JavascriptTextWriter.ExpressionFlags.SpaceAfterBracketsRecommended, + SpaceAroundBracketsRecommended = JavascriptTextWriter.ExpressionFlags.SpaceAroundBracketsRecommended, + + IsMethod = 1 << 16, + + InOperatorIsAmbiguousInDeclaration = 1 << 24, // automatically propagated to sub-expressions + + IsLeftMostInArrowFunctionBody = 1 << 25, // automatically combined and propagated to sub-expressions + IsInsideArrowFunctionBody = 1 << 26, // automatically propagated to sub-expressions + + // https://stackoverflow.com/a/17587899/8656352 + IsLeftMostInNewCallee = 1 << 27, // automatically combined and propagated to sub-expressions + IsInsideNewCallee = 1 << 28, // automatically propagated to sub-expressions + + IsLeftMostInLeftHandSideExpression = 1 << 29, // automatically combined and propagated to sub-expressions + IsInsideLeftHandSideExpression = 1 << 30, // automatically propagated to sub-expressions + + IsInsideStatementExpression = 1 << 31, // automatically propagated to sub-expressions + + IsInPotentiallyAmbiguousContext = InOperatorIsAmbiguousInDeclaration | IsInsideArrowFunctionBody | IsInsideNewCallee | IsInsideLeftHandSideExpression | IsInsideStatementExpression, + } +} diff --git a/src/Esprima/Utils/AstToJavascriptConverter.Helpers.cs b/src/Esprima/Utils/AstToJavascriptConverter.Helpers.cs new file mode 100644 index 00000000..cee8d1bf --- /dev/null +++ b/src/Esprima/Utils/AstToJavascriptConverter.Helpers.cs @@ -0,0 +1,412 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Esprima.Ast; +using static Esprima.Utils.JavascriptTextWriter; + +namespace Esprima.Utils; + +partial class AstToJavascriptConverter +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected static StatementFlags StatementBodyFlags(bool isRightMost) + { + return StatementFlags.IsStatementBody | isRightMost.ToFlag(StatementFlags.IsRightMost); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected static TokenFlags StatementBodyFlagsToKeywordFlags(StatementFlags previousBodyFlags) + { + // Maps IsStatementBody to keyword flags. + return (TokenFlags) (previousBodyFlags & StatementFlags.IsStatementBody); + } + + protected StatementFlags PropagateStatementFlags(StatementFlags flags) + { + // Caller must not set NeedsSemicolon or MayOmitRightMostSemicolon. + // NeedsSemicolon is set by the visitation handler of statement via the StatementNeedsSemicolon method, + // MayOmitRightMostSemicolon is set by VisitStatementList. + Debug.Assert((flags & (StatementFlags.NeedsSemicolon | StatementFlags.MayOmitRightMostSemicolon)) == 0); + + // Combines IsRightMost of parent and current statement to determine its effective value for the current statement list. + flags &= ~StatementFlags.IsRightMost | _currentStatementFlags & StatementFlags.IsRightMost; + + // Propagates MayOmitRightMostSemicolon to current statement. + flags |= _currentStatementFlags & StatementFlags.MayOmitRightMostSemicolon; + + return flags; + } + + private protected static readonly Func s_getCombinedStatementFlags = static (@this, statement, flags) => + @this.PropagateStatementFlags(flags); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void VisitStatement(Statement statement, StatementFlags flags) + { + VisitStatement(statement, flags, s_getCombinedStatementFlags); + } + + protected void VisitStatement(Statement statement, StatementFlags flags, Func getCombinedFlags) + { + var originalStatementFlags = _currentStatementFlags; + _currentStatementFlags = getCombinedFlags(this, statement, flags); + + Writer.StartStatement((JavascriptTextWriter.StatementFlags) _currentStatementFlags, ref _writeContext); + Visit(statement); + Writer.EndStatement((JavascriptTextWriter.StatementFlags) _currentStatementFlags, ref _writeContext); + + _currentStatementFlags = originalStatementFlags; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void VisitStatementList(in NodeList statementList) + { + VisitStatementList(in statementList, static (_, _, index, count) => + (index == count - 1).ToFlag(StatementFlags.IsRightMost | StatementFlags.MayOmitRightMostSemicolon)); + } + + protected void VisitStatementList(in NodeList statementList, Func getCombinedItemFlags) + { + Writer.StartStatementList(statementList.Count, ref _writeContext); + + for (var i = 0; i < statementList.Count; i++) + { + VisitStatementListItem(statementList[i], i, statementList.Count, getCombinedItemFlags); + } + + Writer.EndStatementList(statementList.Count, ref _writeContext); + } + + protected void VisitStatementListItem(Statement statement, int index, int count, Func getCombinedFlags) + { + var originalStatementFlags = _currentStatementFlags; + _currentStatementFlags = getCombinedFlags(this, statement, index, count); + + Writer.StartStatementListItem(index, count, (JavascriptTextWriter.StatementFlags) _currentStatementFlags, ref _writeContext); + Visit(statement); + Writer.EndStatementListItem(index, count, (JavascriptTextWriter.StatementFlags) _currentStatementFlags, ref _writeContext); + + _currentStatementFlags = originalStatementFlags; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void StatementNeedsSemicolon() => _currentStatementFlags |= StatementFlags.NeedsSemicolon; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected static ExpressionFlags RootExpressionFlags(bool needsBrackets) + { + return ExpressionFlags.IsLeftMost | needsBrackets.ToFlag(ExpressionFlags.NeedsBrackets); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected static ExpressionFlags LeftHandSideRootExpressionFlags(bool needsBrackets) + { + return ExpressionFlags.IsInsideLeftHandSideExpression | ExpressionFlags.IsLeftMostInLeftHandSideExpression | RootExpressionFlags(needsBrackets); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected static ExpressionFlags SubExpressionFlags(bool needsBrackets, bool isLeftMost) + { + return needsBrackets.ToFlag(ExpressionFlags.NeedsBrackets) | isLeftMost.ToFlag(ExpressionFlags.IsLeftMost); + } + + protected ExpressionFlags PropagateExpressionFlags(ExpressionFlags flags) + { + const ExpressionFlags isLeftMostFlags = + ExpressionFlags.IsLeftMost | + ExpressionFlags.IsLeftMostInArrowFunctionBody | + ExpressionFlags.IsLeftMostInNewCallee | + ExpressionFlags.IsLeftMostInLeftHandSideExpression; + + // Combines IsLeftMost* flags of parent and current statement to determine their effective values for the current expression tree. + if (_currentExpressionFlags.HasFlagFast(ExpressionFlags.NeedsBrackets) || !flags.HasFlagFast(ExpressionFlags.IsLeftMost)) + { + flags &= ~isLeftMostFlags; + } + else + { + flags = flags & ~isLeftMostFlags | _currentExpressionFlags & isLeftMostFlags; + } + + // Propagates IsInsideStatementExpression, IsInsideArrowFunctionBody and IsInsideLeftHandSideExpression to current expression. + flags |= _currentExpressionFlags & ExpressionFlags.IsInPotentiallyAmbiguousContext; + + return flags; + } + + protected ExpressionFlags DisambiguateExpression(Expression expression, ExpressionFlags flags) + { + if (flags.HasFlagFast(ExpressionFlags.NeedsBrackets)) + { + return flags & ~ExpressionFlags.InOperatorIsAmbiguousInDeclaration; + } + + // Puts the left-most expression in brackets if necessary (in cases where it would be interpreted differently without brackets). + if ((flags & ExpressionFlags.IsInPotentiallyAmbiguousContext) != 0) + { + if (flags.HasFlagFast(ExpressionFlags.IsInsideStatementExpression | ExpressionFlags.IsLeftMost) && ExpressionIsAmbiguousAsStatementExpression(expression) || + flags.HasFlagFast(ExpressionFlags.IsInsideLeftHandSideExpression | ExpressionFlags.IsLeftMostInLeftHandSideExpression) && LeftHandSideExpressionIsParenthesized(expression) || + flags.HasFlagFast(ExpressionFlags.IsInsideArrowFunctionBody | ExpressionFlags.IsLeftMostInArrowFunctionBody) && ExpressionIsAmbiguousAsArrowFunctionBody(expression) || + flags.HasFlagFast(ExpressionFlags.IsInsideNewCallee | ExpressionFlags.IsLeftMostInNewCallee) && ExpressionIsAmbiguousAsNewCallee(expression)) + { + return (flags | ExpressionFlags.NeedsBrackets) & ~ExpressionFlags.InOperatorIsAmbiguousInDeclaration; + } + // Edge case: for (var a = b = (c in d in e) in x); + else if (flags.HasFlagFast(ExpressionFlags.InOperatorIsAmbiguousInDeclaration) && expression is BinaryExpression { Operator: BinaryOperator.In }) + { + return (flags | ExpressionFlags.NeedsBrackets) & ~ExpressionFlags.InOperatorIsAmbiguousInDeclaration; + } + } + + return flags; + } + + private protected static readonly Func s_getCombinedRootExpressionFlags = static (@this, expression, flags) => + @this.DisambiguateExpression(expression, flags); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void VisitRootExpression(Expression expression, ExpressionFlags flags) + { + VisitExpression(expression, flags, s_getCombinedRootExpressionFlags); + } + + private protected static readonly Func s_getCombinedSubExpressionFlags = static (@this, expression, flags) => + @this.DisambiguateExpression(expression, @this.PropagateExpressionFlags(flags)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void VisitSubExpression(Expression expression, ExpressionFlags flags) + { + VisitExpression(expression, flags, s_getCombinedSubExpressionFlags); + } + + protected void VisitExpression(Expression expression, ExpressionFlags flags, Func getCombinedFlags) + { + var originalExpressionFlags = _currentExpressionFlags; + _currentExpressionFlags = getCombinedFlags(this, expression, flags); + + Writer.StartExpression((JavascriptTextWriter.ExpressionFlags) _currentExpressionFlags, ref _writeContext); + Visit(expression); + Writer.EndExpression((JavascriptTextWriter.ExpressionFlags) _currentExpressionFlags, ref _writeContext); + + _currentExpressionFlags = originalExpressionFlags; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void VisitSubExpressionList(in NodeList expressionList) + { + VisitExpressionList(in expressionList, static (@this, expression, index, _) => + s_getCombinedSubExpressionFlags(@this, expression, SubExpressionFlags(@this.ExpressionNeedsBracketsInList(expression), isLeftMost: false))); + } + + protected void VisitExpressionList(in NodeList expressionList, Func getCombinedItemFlags) + { + Writer.StartExpressionList(expressionList.Count, ref _writeContext); + + for (var i = 0; i < expressionList.Count; i++) + { + VisitExpressionListItem(expressionList[i], i, expressionList.Count, getCombinedItemFlags); + } + + Writer.EndExpressionList(expressionList.Count, ref _writeContext); + } + + protected void VisitExpressionListItem(Expression expression, int index, int count, Func getCombinedFlags) + { + var originalExpressionFlags = _currentExpressionFlags; + _currentExpressionFlags = getCombinedFlags(this, expression, index, count); + + Writer.StartExpressionListItem(index, count, (JavascriptTextWriter.ExpressionFlags) _currentExpressionFlags, ref _writeContext); + Visit(expression); + Writer.EndExpressionListItem(index, count, (JavascriptTextWriter.ExpressionFlags) _currentExpressionFlags, ref _writeContext); + + _currentExpressionFlags = originalExpressionFlags; + } + + private void VisitAssertions(in NodeList assertions) + { + // https://github.com/tc39/proposal-import-assertions + + Writer.WriteKeyword("assert", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + Writer.StartObject(assertions.Count, ref _writeContext); + + VisitAuxiliaryNodeList(in assertions, separator: ","); + + Writer.EndObject(assertions.Count, ref _writeContext); + } + + private void VisitExportOrImportSpecifierIdentifier(Expression identifierExpression) + { + if (identifierExpression is Identifier identifier && identifier.Name == "default") + { + Writer.WriteKeyword("default", ref _writeContext); + } + else + { + VisitRootExpression(identifierExpression, RootExpressionFlags(needsBrackets: false)); + } + } + + private void VisitPropertyKey(Expression key, bool computed, TokenFlags leadingBracketFlags = TokenFlags.None, TokenFlags trailingBracketFlags = TokenFlags.None) + { + if (computed) + { + Writer.WritePunctuator("[", TokenFlags.Leading | leadingBracketFlags, ref _writeContext); + VisitRootExpression(key, RootExpressionFlags(needsBrackets: ExpressionNeedsBracketsInList(key))); + Writer.WritePunctuator("]", TokenFlags.Trailing | trailingBracketFlags, ref _writeContext); + } + else if (key.Type == Nodes.Identifier) + { + VisitAuxiliaryNode(key); + } + else + { + VisitRootExpression(key, RootExpressionFlags(needsBrackets: false)); + } + } + + protected virtual bool ExpressionIsAmbiguousAsStatementExpression(Expression expression) + { + switch (expression.Type) + { + case Nodes.ClassExpression: + case Nodes.FunctionExpression: + case Nodes.ObjectExpression: + case Nodes.AssignmentExpression when expression.As() is { Left.Type: Nodes.ObjectPattern }: + case Nodes.Identifier when Scanner.IsStrictModeReservedWord(expression.As().Name!): + return true; + } + + return false; + } + + protected virtual bool ExpressionIsAmbiguousAsArrowFunctionBody(Expression expression) + { + switch (expression.Type) + { + case Nodes.ObjectExpression: + case Nodes.AssignmentExpression when expression.As() is { Left.Type: Nodes.ObjectPattern }: + return true; + } + + return false; + } + + protected virtual bool ExpressionIsAmbiguousAsNewCallee(Expression expression) + { + switch (expression.Type) + { + case Nodes.CallExpression: + return true; + } + + return false; + } + + protected virtual bool LeftHandSideExpressionIsParenthesized(Expression expression) + { + // https://tc39.es/ecma262/#sec-left-hand-side-expressions + + switch (expression.Type) + { + case Nodes.ArrowFunctionExpression: + case Nodes.AssignmentExpression: + case Nodes.AwaitExpression: + case Nodes.BinaryExpression: + case Nodes.LogicalExpression: + case Nodes.ConditionalExpression: + case Nodes.SequenceExpression: + case Nodes.UnaryExpression: + case Nodes.UpdateExpression: + case Nodes.YieldExpression: + return true; + } + + return false; + } + + protected virtual bool ExpressionNeedsBracketsInList(Expression expression) + { + return expression.Type is + Nodes.SequenceExpression; + } + + protected virtual int GetOperatorPrecedence(Expression expression, out int associativity) => + expression.GetOperatorPrecedence(out associativity) is >= 0 and var result + ? result + : throw new NotImplementedException($"Operator precedence for expression of type {expression.GetType()} is not defined."); + + protected bool UnaryOperandNeedsBrackets(Expression operation, Expression operand) => + GetOperatorPrecedence(operation, out _) > GetOperatorPrecedence(operand, out _); + + protected BinaryOperationFlags BinaryOperandsNeedBrackets(Expression operation, Expression leftOperand, Expression rightOperand) + { + var operationPrecedence = GetOperatorPrecedence(operation, out var associativity); + var leftOperandPrecedence = GetOperatorPrecedence(leftOperand, out _); + var rightOperandPrecedence = GetOperatorPrecedence(rightOperand, out _); + + var result = BinaryOperationFlags.None; + + if (operationPrecedence > leftOperandPrecedence || operationPrecedence == leftOperandPrecedence && associativity > 0) // right-to-left associativity + { + result |= BinaryOperationFlags.LeftOperandNeedsBrackets; + } + + if (operationPrecedence > rightOperandPrecedence || operationPrecedence == rightOperandPrecedence && associativity < 0) // left-to-right associativity + { + result |= BinaryOperationFlags.RightOperandNeedsBrackets; + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void VisitAuxiliaryNode(Node node) + { + VisitAuxiliaryNode(node, static delegate { return null; }); + } + + protected void VisitAuxiliaryNode(Node node, Func getNodeContext) + { + var originalAuxiliaryNodeContext = _currentAuxiliaryNodeContext; + _currentAuxiliaryNodeContext = getNodeContext(this, node); + + Writer.StartAuxiliaryNode(_currentAuxiliaryNodeContext, ref _writeContext); + Visit(node); + Writer.EndAuxiliaryNode(_currentAuxiliaryNodeContext, ref _writeContext); + + _currentAuxiliaryNodeContext = originalAuxiliaryNodeContext; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void VisitAuxiliaryNodeList(in NodeList nodeList, string separator) + where TNode : Node + { + VisitAuxiliaryNodeList(in nodeList, separator, static delegate { return null; }); + } + + protected void VisitAuxiliaryNodeList(in NodeList nodeList, string separator, Func getNodeContext) + where TNode : Node + { + Writer.StartAuxiliaryNodeList(nodeList.Count, ref _writeContext); + + for (var i = 0; i < nodeList.Count; i++) + { + VisitAuxiliaryNodeListItem(nodeList[i], i, nodeList.Count, separator, getNodeContext); + } + + Writer.EndAuxiliaryNodeList(nodeList.Count, ref _writeContext); + } + + protected void VisitAuxiliaryNodeListItem(TNode node, int index, int count, string separator, Func getNodeContext) + where TNode : Node + { + var originalAuxiliaryNodeContext = _currentAuxiliaryNodeContext; + _currentAuxiliaryNodeContext = getNodeContext(this, node, index, count); + + Writer.StartAuxiliaryNodeListItem(index, count, separator, _currentAuxiliaryNodeContext, ref _writeContext); + Visit(node); + Writer.EndAuxiliaryNodeListItem(index, count, separator, _currentAuxiliaryNodeContext, ref _writeContext); + + _currentAuxiliaryNodeContext = originalAuxiliaryNodeContext; + } +} diff --git a/src/Esprima/Utils/AstToJavascriptConverter.cs b/src/Esprima/Utils/AstToJavascriptConverter.cs new file mode 100644 index 00000000..fc257321 --- /dev/null +++ b/src/Esprima/Utils/AstToJavascriptConverter.cs @@ -0,0 +1,1663 @@ +using System.Runtime.CompilerServices; +using Esprima.Ast; +using static Esprima.Utils.JavascriptTextWriter; + +namespace Esprima.Utils; + +public partial class AstToJavascriptConverter : AstVisitor +{ + // Notes for maintainers: + // Don't visit nodes directly (by calling Visit) unless it's necessary for some special reason (but in that case you'll need to setup the context of the visitation manually!) + // For examples of special reason, see VisitArrayExpression, VisitObjectExpression, VisitImport, etc. In usual cases just use the following predefined visitation helper methods: + // * Visit statements using VisitStatement / VisitStatementList. + // * Visit expressions using VisitRootExpression and sub-expressions (expressions inside another expression) using VisitSubExpression / VisitSubExpressionList. + // * Visit identifiers using VisitAuxiliaryNode when they are binding identifiers (declarations) and visit them using VisitRootExpression when they are identifier references (actual expressions). + // * Visit any other nodes using VisitAuxiliaryNode / VisitAuxiliaryNodeList. + + private static readonly object s_lastSwitchCaseFlag = new(); + private static readonly object s_forLoopInitDeclarationFlag = new(); + + private WriteContext _writeContext; + private StatementFlags _currentStatementFlags; + private ExpressionFlags _currentExpressionFlags; + private object? _currentAuxiliaryNodeContext; + + public AstToJavascriptConverter(JavascriptTextWriter writer, AstToJavascriptOptions options) + { + Writer = writer ?? throw new ArgumentNullException(nameof(writer)); + + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + } + + public JavascriptTextWriter Writer { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } + + protected ref readonly WriteContext WriteContext { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref _writeContext; } + + protected Node? ParentNode { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _writeContext.ParentNode; } + + public void Convert(Node node) + { + _writeContext = default; + _currentStatementFlags = StatementFlags.None; + _currentExpressionFlags = ExpressionFlags.None; + _currentAuxiliaryNodeContext = null; + + Visit(node ?? throw new ArgumentNullException(nameof(node))); + } + + public override object? Visit(Node node) + { + var originalWriteContext = _writeContext; + _writeContext = new WriteContext(originalWriteContext.Node, node); + + var result = base.Visit(node); + + _writeContext = originalWriteContext; + + return result; + } + + protected internal override object? VisitArrayExpression(ArrayExpression arrayExpression) + { + _writeContext.SetNodeProperty(nameof(arrayExpression.Elements), static node => ref node.As().Elements); + + Writer.StartArray(arrayExpression.Elements.Count, ref _writeContext); + + // Elements need special care because it may contain null values denoting omitted elements. + + Writer.StartExpressionList(arrayExpression.Elements.Count, ref _writeContext); + + for (var i = 0; i < arrayExpression.Elements.Count; i++) + { + var element = arrayExpression.Elements[i]; + + if (element is not null) + { + VisitExpressionListItem(element, i, arrayExpression.Elements.Count, static (@this, expression, index, _) => + s_getCombinedSubExpressionFlags(@this, expression, SubExpressionFlags(@this.ExpressionNeedsBracketsInList(expression), isLeftMost: false))); + } + else + { + var originalExpressionFlags = _currentExpressionFlags; + _currentExpressionFlags = PropagateExpressionFlags(SubExpressionFlags(needsBrackets: false, isLeftMost: false)); + + Writer.StartExpressionListItem(i, arrayExpression.Elements.Count, (JavascriptTextWriter.ExpressionFlags) _currentExpressionFlags, ref _writeContext); + Writer.EndExpressionListItem(i, arrayExpression.Elements.Count, (JavascriptTextWriter.ExpressionFlags) _currentExpressionFlags, ref _writeContext); + + _currentExpressionFlags = originalExpressionFlags; + } + } + + Writer.EndExpressionList(arrayExpression.Elements.Count, ref _writeContext); + + Writer.EndArray(arrayExpression.Elements.Count, ref _writeContext); + + return arrayExpression; + } + + protected internal override object? VisitArrayPattern(ArrayPattern arrayPattern) + { + _writeContext.SetNodeProperty(nameof(arrayPattern.Elements), static node => ref node.As().Elements); + + Writer.StartArray(arrayPattern.Elements.Count, ref _writeContext); + + // Elements need special care because it may contain null values denoting omitted elements. + + Writer.StartAuxiliaryNodeList(arrayPattern.Elements.Count, ref _writeContext); + + for (var i = 0; i < arrayPattern.Elements.Count; i++) + { + var element = arrayPattern.Elements[i]; + + var originalAuxiliaryNodeContext = _currentAuxiliaryNodeContext; + _currentAuxiliaryNodeContext = null; + + Writer.StartAuxiliaryNodeListItem(i, arrayPattern.Elements.Count, separator: ",", _currentAuxiliaryNodeContext, ref _writeContext); + if (element is not null) + { + Visit(element); + } + Writer.EndAuxiliaryNodeListItem(i, arrayPattern.Elements.Count, separator: ",", _currentAuxiliaryNodeContext, ref _writeContext); + + _currentAuxiliaryNodeContext = originalAuxiliaryNodeContext; + } + + Writer.EndAuxiliaryNodeList(arrayPattern.Elements.Count, ref _writeContext); + + Writer.EndArray(arrayPattern.Elements.Count, ref _writeContext); + + return arrayPattern; + } + + protected internal override object? VisitArrowFunctionExpression(ArrowFunctionExpression arrowFunctionExpression) + { + if (arrowFunctionExpression.Async) + { + _writeContext.SetNodeProperty(nameof(arrowFunctionExpression.Async), static node => node.As().Async); + Writer.WriteKeyword("async", TokenFlags.TrailingSpaceRecommended, ref _writeContext); + } + + _writeContext.SetNodeProperty(nameof(arrowFunctionExpression.Params), static node => ref node.As().Params); + + if (arrowFunctionExpression.Params.Count == 1 && arrowFunctionExpression.Params[0].Type == Nodes.Identifier) + { + VisitAuxiliaryNodeList(in arrowFunctionExpression.Params, separator: ","); + } + else + { + Writer.WritePunctuator("(", TokenFlags.Leading, ref _writeContext); + VisitAuxiliaryNodeList(in arrowFunctionExpression.Params, separator: ","); + Writer.WritePunctuator(")", TokenFlags.Trailing, ref _writeContext); + } + + _writeContext.ClearNodeProperty(); + Writer.WritePunctuator("=>", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(arrowFunctionExpression.Body), static node => node.As().Body); + if (arrowFunctionExpression.Body is BlockStatement bodyBlockStatement) + { + VisitStatement(bodyBlockStatement, StatementFlags.IsRightMost); + } + else + { + var bodyExpression = arrowFunctionExpression.Body.As(); + var bodyNeedsBrackets = UnaryOperandNeedsBrackets(arrowFunctionExpression, bodyExpression); + VisitExpression(bodyExpression, SubExpressionFlags(bodyNeedsBrackets, isLeftMost: false), static (@this, expression, flags) => + @this.DisambiguateExpression(expression, ExpressionFlags.IsInsideArrowFunctionBody | ExpressionFlags.IsLeftMostInArrowFunctionBody | @this.PropagateExpressionFlags(flags))); + } + + return arrowFunctionExpression; + } + + protected internal override object? VisitAssignmentExpression(AssignmentExpression assignmentExpression) + { + _writeContext.SetNodeProperty(nameof(assignmentExpression.Left), static node => node.As().Left); + VisitAuxiliaryNode(assignmentExpression.Left); + + var op = AssignmentExpression.GetAssignmentOperatorToken(assignmentExpression.Operator); + + _writeContext.SetNodeProperty(nameof(assignmentExpression.Operator), static node => node.As().Operator); + Writer.WritePunctuator(op, TokenFlags.InBetween | TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + // AssignmentExpression is not a real binary operation because its left side is not an expression. + var rightNeedsBrackets = GetOperatorPrecedence(assignmentExpression, out _) > GetOperatorPrecedence(assignmentExpression.Right, out _); + + _writeContext.SetNodeProperty(nameof(assignmentExpression.Right), static node => node.As().Right); + VisitSubExpression(assignmentExpression.Right, SubExpressionFlags(rightNeedsBrackets, isLeftMost: false)); + + return assignmentExpression; + } + + protected internal override object? VisitAssignmentPattern(AssignmentPattern assignmentPattern) + { + _writeContext.SetNodeProperty(nameof(assignmentPattern.Left), static node => node.As().Left); + VisitAuxiliaryNode(assignmentPattern.Left); + + _writeContext.ClearNodeProperty(); + Writer.WritePunctuator("=", TokenFlags.InBetween | TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(assignmentPattern.Right), static node => node.As().Right); + VisitAuxiliaryNode(assignmentPattern.Right); + + return assignmentPattern; + } + + protected internal override object? VisitAwaitExpression(AwaitExpression awaitExpression) + { + Writer.WriteKeyword("await", TokenFlags.TrailingSpaceRecommended, ref _writeContext); + + var argumentNeedsBrackets = UnaryOperandNeedsBrackets(awaitExpression, awaitExpression.Argument); + + _writeContext.SetNodeProperty(nameof(awaitExpression.Argument), static node => node.As().Argument); + VisitSubExpression(awaitExpression.Argument, SubExpressionFlags(argumentNeedsBrackets, isLeftMost: false)); + + return awaitExpression; + } + + protected internal override object? VisitBinaryExpression(BinaryExpression binaryExpression) + { + var operationFlags = BinaryOperandsNeedBrackets(binaryExpression, binaryExpression.Left, binaryExpression.Right); + + // The operand of unary operators cannot be an exponentiation without grouping. + // E.g. -1 ** 2 is syntactically unambiguous but the language requires (-1) ** 2 instead. + if (!operationFlags.HasFlagFast(BinaryOperationFlags.LeftOperandNeedsBrackets) && + binaryExpression.Operator == BinaryOperator.Exponentiation && + binaryExpression.Left is UnaryExpression leftUnaryExpression) + { + operationFlags |= BinaryOperationFlags.LeftOperandNeedsBrackets; + } + + _writeContext.SetNodeProperty(nameof(binaryExpression.Left), static node => node.As().Left); + VisitSubExpression(binaryExpression.Left, SubExpressionFlags(operationFlags.HasFlagFast(BinaryOperationFlags.LeftOperandNeedsBrackets), isLeftMost: true)); + + var op = BinaryExpression.GetBinaryOperatorToken(binaryExpression.Operator); + + _writeContext.SetNodeProperty(nameof(binaryExpression.Operator), static node => node.As().Operator); + if (char.IsLetter(op[0])) + { + Writer.WriteKeyword(op, TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + } + else + { + Writer.WritePunctuator(op, TokenFlags.InBetween | TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + // Cases like 1 + (+x) must be disambiguated with brackets. + if (!operationFlags.HasFlagFast(BinaryOperationFlags.RightOperandNeedsBrackets) && + binaryExpression.Right is UnaryExpression rightUnaryExpression && + rightUnaryExpression.Prefix && + op[op.Length - 1] is '+' or '-' && + op[op.Length - 1] == UnaryExpression.GetUnaryOperatorToken(rightUnaryExpression.Operator)[0]) + { + operationFlags |= BinaryOperationFlags.RightOperandNeedsBrackets; + } + } + + _writeContext.SetNodeProperty(nameof(binaryExpression.Right), static node => node.As().Right); + VisitSubExpression(binaryExpression.Right, SubExpressionFlags(operationFlags.HasFlagFast(BinaryOperationFlags.RightOperandNeedsBrackets), isLeftMost: false)); + + return binaryExpression; + } + + protected internal override object? VisitBlockStatement(BlockStatement blockStatement) + { + _writeContext.SetNodeProperty(nameof(blockStatement.Body), static node => ref node.As().Body); + Writer.StartBlock(blockStatement.Body.Count, ref _writeContext); + + VisitStatementList(in blockStatement.Body); + + Writer.EndBlock(blockStatement.Body.Count, ref _writeContext); + + return blockStatement; + } + + protected internal override object? VisitBreakStatement(BreakStatement breakStatement) + { + Writer.WriteKeyword("break", TokenFlags.LeadingSpaceRecommended, ref _writeContext); + + if (breakStatement.Label is not null) + { + _writeContext.SetNodeProperty(nameof(breakStatement.Label), static node => node.As().Label); + VisitRootExpression(breakStatement.Label, RootExpressionFlags(needsBrackets: false)); + } + + StatementNeedsSemicolon(); + + return breakStatement; + } + + protected internal override object? VisitCallExpression(CallExpression callExpression) + { + var calleeNeedsBrackets = UnaryOperandNeedsBrackets(callExpression, callExpression.Callee); + + _writeContext.SetNodeProperty(nameof(callExpression.Callee), static node => node.As().Callee); + VisitSubExpression(callExpression.Callee, SubExpressionFlags(calleeNeedsBrackets, isLeftMost: true)); + + if (callExpression.Optional) + { + _writeContext.ClearNodeProperty(); + Writer.WritePunctuator("?.", TokenFlags.InBetween, ref _writeContext); + } + + _writeContext.SetNodeProperty(nameof(callExpression.Arguments), static node => ref node.As().Arguments); + Writer.WritePunctuator("(", TokenFlags.Leading, ref _writeContext); + VisitSubExpressionList(in callExpression.Arguments); + Writer.WritePunctuator(")", TokenFlags.Trailing, ref _writeContext); + + return callExpression; + } + + protected internal override object? VisitCatchClause(CatchClause catchClause) + { + if (catchClause.Param is not null) + { + _writeContext.SetNodeProperty(nameof(catchClause.Param), static node => node.As().Param); + Writer.WritePunctuator("(", TokenFlags.Leading | TokenFlags.LeadingSpaceRecommended, ref _writeContext); + VisitAuxiliaryNode(catchClause.Param); + Writer.WritePunctuator(")", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref _writeContext); + } + + _writeContext.SetNodeProperty(nameof(catchClause.Body), static node => node.As().Body); + VisitStatement(catchClause.Body, StatementBodyFlags(isRightMost: ParentNode!.As().Finalizer is null)); + + return catchClause; + } + + protected internal override object? VisitChainExpression(ChainExpression chainExpression) + { + _writeContext.SetNodeProperty(nameof(chainExpression.Expression), static node => node.As().Expression); + VisitSubExpression(chainExpression.Expression, SubExpressionFlags(needsBrackets: false, isLeftMost: true)); + + return chainExpression; + } + + protected internal override object? VisitClassBody(ClassBody classBody) + { + _writeContext.SetNodeProperty(nameof(classBody.Body), static node => ref node.As().Body); + Writer.StartBlock(classBody.Body.Count, ref _writeContext); + + VisitAuxiliaryNodeList(in classBody.Body, separator: string.Empty); + + Writer.EndBlock(classBody.Body.Count, ref _writeContext); + + return classBody; + } + + protected internal override object? VisitClassDeclaration(ClassDeclaration classDeclaration) + { + if (classDeclaration.Decorators.Count > 0) + { + _writeContext.SetNodeProperty(nameof(classDeclaration.Decorators), static node => ref node.As().Decorators); + VisitAuxiliaryNodeList(classDeclaration.Decorators, separator: string.Empty); + + _writeContext.ClearNodeProperty(); + } + + Writer.WriteKeyword("class", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + if (classDeclaration.Id is not null) + { + _writeContext.SetNodeProperty(nameof(classDeclaration.Id), static node => node.As().Id); + VisitAuxiliaryNode(classDeclaration.Id); + } + + if (classDeclaration.SuperClass is not null) + { + _writeContext.ClearNodeProperty(); + Writer.WriteKeyword("extends", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(classDeclaration.SuperClass), static node => node.As().SuperClass); + VisitRootExpression(classDeclaration.SuperClass, LeftHandSideRootExpressionFlags(needsBrackets: false)); + } + + _writeContext.SetNodeProperty(nameof(classDeclaration.Body), static node => node.As().Body); + VisitAuxiliaryNode(classDeclaration.Body); + + return classDeclaration; + } + + protected internal override object? VisitClassExpression(ClassExpression classExpression) + { + if (classExpression.Decorators.Count > 0) + { + _writeContext.SetNodeProperty(nameof(classExpression.Decorators), static node => ref node.As().Decorators); + VisitAuxiliaryNodeList(classExpression.Decorators, separator: string.Empty); + + _writeContext.ClearNodeProperty(); + } + + Writer.WriteKeyword("class", TokenFlags.TrailingSpaceRecommended, ref _writeContext); + + if (classExpression.Id is not null) + { + _writeContext.SetNodeProperty(nameof(classExpression.Id), static node => node.As().Id); + VisitAuxiliaryNode(classExpression.Id); + } + + if (classExpression.SuperClass is not null) + { + _writeContext.ClearNodeProperty(); + Writer.WriteKeyword("extends", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(classExpression.SuperClass), static node => node.As().SuperClass); + VisitRootExpression(classExpression.SuperClass, LeftHandSideRootExpressionFlags(needsBrackets: false)); + } + + _writeContext.SetNodeProperty(nameof(classExpression.Body), static node => node.As().Body); + VisitAuxiliaryNode(classExpression.Body); + + return classExpression; + } + + protected internal override object? VisitConditionalExpression(ConditionalExpression conditionalExpression) + { + // Test expressions with the same precendence as ternary operator (such as nested conditional expression, assignment, yield, etc.) also needs brackets. + var operandNeedsBrackets = GetOperatorPrecedence(conditionalExpression, out _) >= GetOperatorPrecedence(conditionalExpression.Test, out _); + + _writeContext.SetNodeProperty(nameof(conditionalExpression.Test), static node => node.As().Test); + VisitSubExpression(conditionalExpression.Test, SubExpressionFlags(operandNeedsBrackets, isLeftMost: true)); + + // Consequent expressions with the same precendence as ternary operator are unambiguous without brackets. + operandNeedsBrackets = GetOperatorPrecedence(conditionalExpression, out _) > GetOperatorPrecedence(conditionalExpression.Consequent, out _); + + _writeContext.SetNodeProperty(nameof(conditionalExpression.Consequent), static node => node.As().Consequent); + Writer.WritePunctuator("?", TokenFlags.Leading | TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + VisitExpression(conditionalExpression.Consequent, SubExpressionFlags(operandNeedsBrackets, isLeftMost: false), static (@this, expression, flags) => + // Edge case: 'in' operators in for...in loop declarations are not ambigous when they are in the consequent part of the conditional expression. + @this.DisambiguateExpression(expression, ~ExpressionFlags.InOperatorIsAmbiguousInDeclaration & @this.PropagateExpressionFlags(flags))); + + // Alternate expressions with the same precendence as ternary operator are unambiguous without brackets, even conditional expressions because of right-to-left associativity. + operandNeedsBrackets = GetOperatorPrecedence(conditionalExpression, out _) > GetOperatorPrecedence(conditionalExpression.Alternate, out _); + + _writeContext.SetNodeProperty(nameof(conditionalExpression.Alternate), static node => node.As().Alternate); + Writer.WritePunctuator(":", TokenFlags.Leading | TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + VisitSubExpression(conditionalExpression.Alternate, SubExpressionFlags(operandNeedsBrackets, isLeftMost: false)); + + return conditionalExpression; + } + + protected internal override object? VisitContinueStatement(ContinueStatement continueStatement) + { + Writer.WriteKeyword("continue", TokenFlags.LeadingSpaceRecommended, ref _writeContext); + + if (continueStatement.Label is not null) + { + _writeContext.SetNodeProperty(nameof(continueStatement.Label), static node => node.As().Label); + VisitRootExpression(continueStatement.Label, RootExpressionFlags(needsBrackets: false)); + } + + StatementNeedsSemicolon(); + + return continueStatement; + } + + protected internal override object? VisitDebuggerStatement(DebuggerStatement debuggerStatement) + { + Writer.WriteKeyword("debugger", TokenFlags.LeadingSpaceRecommended, ref _writeContext); + + StatementNeedsSemicolon(); + + return debuggerStatement; + } + + protected internal override object? VisitDecorator(Decorator decorator) + { + // https://github.com/tc39/proposal-decorators + + Writer.WritePunctuator("@", TokenFlags.Leading | (ParentNode is not Expression).ToFlag(TokenFlags.LeadingSpaceRecommended), ref _writeContext); + + _writeContext.SetNodeProperty(nameof(decorator.Expression), static node => node.As().Expression); + VisitRootExpression(decorator.Expression, LeftHandSideRootExpressionFlags(needsBrackets: false)); + + Writer.WriteEpsilon(TokenFlags.TrailingSpaceRecommended, ref _writeContext); + + return decorator; + } + + protected internal override object? VisitDoWhileStatement(DoWhileStatement doWhileStatement) + { + Writer.WriteKeyword("do", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(doWhileStatement.Body), static node => node.As().Body); + StatementFlags bodyFlags; + VisitStatement(doWhileStatement.Body, bodyFlags = StatementBodyFlags(isRightMost: false)); + + _writeContext.ClearNodeProperty(); + Writer.WriteKeyword("while", TokenFlags.SurroundingSpaceRecommended | StatementBodyFlagsToKeywordFlags(bodyFlags), ref _writeContext); + + _writeContext.SetNodeProperty(nameof(doWhileStatement.Test), static node => node.As().Test); + VisitRootExpression(doWhileStatement.Test, ExpressionFlags.SpaceBeforeBracketsRecommended | RootExpressionFlags(needsBrackets: true)); + + return doWhileStatement; + } + + protected internal override object? VisitEmptyStatement(EmptyStatement emptyStatement) + { + Writer.WritePunctuator(";", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + return emptyStatement; + } + + protected internal override object? VisitExportAllDeclaration(ExportAllDeclaration exportAllDeclaration) + { + Writer.WriteKeyword("export", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + Writer.WritePunctuator("*", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + if (exportAllDeclaration.Exported is not null) + { + _writeContext.SetNodeProperty(nameof(exportAllDeclaration.Exported), static node => node.As().Exported); + Writer.WriteKeyword("as", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + VisitExportOrImportSpecifierIdentifier(exportAllDeclaration.Exported); + } + + _writeContext.ClearNodeProperty(); + Writer.WriteKeyword("from", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(exportAllDeclaration.Source), static node => node.As().Source); + VisitRootExpression(exportAllDeclaration.Source, RootExpressionFlags(needsBrackets: false)); + + if (exportAllDeclaration.Assertions.Count > 0) + { + _writeContext.SetNodeProperty(nameof(exportAllDeclaration.Assertions), static node => ref node.As().Assertions); + VisitAssertions(in exportAllDeclaration.Assertions); + } + + StatementNeedsSemicolon(); + + return exportAllDeclaration; + } + + protected internal override object? VisitExportDefaultDeclaration(ExportDefaultDeclaration exportDefaultDeclaration) + { + Writer.WriteKeyword("export", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + Writer.WriteKeyword("default", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(exportDefaultDeclaration.Declaration), static node => node.As().Declaration); + if (exportDefaultDeclaration.Declaration is Declaration declaration) + { + VisitStatement(declaration, StatementFlags.IsRightMost); + } + else + { + VisitRootExpression(exportDefaultDeclaration.Declaration.As(), ExpressionFlags.IsInsideStatementExpression | RootExpressionFlags(needsBrackets: false)); + + StatementNeedsSemicolon(); + } + + return exportDefaultDeclaration; + } + + protected internal override object? VisitExportNamedDeclaration(ExportNamedDeclaration exportNamedDeclaration) + { + Writer.WriteKeyword("export", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + if (exportNamedDeclaration.Declaration is not null) + { + _writeContext.SetNodeProperty(nameof(exportNamedDeclaration.Declaration), static node => node.As().Declaration); + VisitStatement(exportNamedDeclaration.Declaration.As(), StatementFlags.IsRightMost); + } + else + { + _writeContext.SetNodeProperty(nameof(exportNamedDeclaration.Specifiers), static node => ref node.As().Specifiers); + Writer.WritePunctuator("{", TokenFlags.Leading | TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + VisitAuxiliaryNodeList(in exportNamedDeclaration.Specifiers, separator: ","); + Writer.WritePunctuator("}", TokenFlags.Trailing | TokenFlags.LeadingSpaceRecommended, ref _writeContext); + + if (exportNamedDeclaration.Source is not null) + { + _writeContext.ClearNodeProperty(); + Writer.WriteKeyword("from", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(exportNamedDeclaration.Source), static node => node.As().Source); + VisitRootExpression(exportNamedDeclaration.Source, RootExpressionFlags(needsBrackets: false)); + + if (exportNamedDeclaration.Assertions.Count > 0) + { + _writeContext.SetNodeProperty(nameof(exportNamedDeclaration.Assertions), static node => ref node.As().Assertions); + VisitAssertions(in exportNamedDeclaration.Assertions); + } + } + + StatementNeedsSemicolon(); + } + + return exportNamedDeclaration; + } + + protected internal override object? VisitExportSpecifier(ExportSpecifier exportSpecifier) + { + _writeContext.SetNodeProperty(nameof(exportSpecifier.Local), static node => node.As().Local); + VisitExportOrImportSpecifierIdentifier(exportSpecifier.Local); + + if (exportSpecifier.Local != exportSpecifier.Exported) + { + _writeContext.ClearNodeProperty(); + Writer.WriteKeyword("as", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(exportSpecifier.Exported), static node => node.As().Exported); + VisitExportOrImportSpecifierIdentifier(exportSpecifier.Exported); + } + + return exportSpecifier; + } + + protected internal override object? VisitExpressionStatement(ExpressionStatement expressionStatement) + { + _writeContext.SetNodeProperty(nameof(expressionStatement.Expression), static node => node.As().Expression); + Writer.WriteEpsilon(TokenFlags.LeadingSpaceRecommended, ref _writeContext); + VisitRootExpression(expressionStatement.Expression, ExpressionFlags.IsInsideStatementExpression | RootExpressionFlags(needsBrackets: false)); + + StatementNeedsSemicolon(); + + return expressionStatement; + } + + protected internal override object? VisitForInStatement(ForInStatement forInStatement) + { + Writer.WriteKeyword("for", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + Writer.WritePunctuator("(", TokenFlags.Leading | TokenFlags.LeadingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(forInStatement.Left), static node => node.As().Left); + + if (forInStatement.Left is VariableDeclaration variableDeclaration) + { + VisitStatement(variableDeclaration, StatementFlags.NestedVariableDeclaration); + } + else + { + VisitAuxiliaryNode(forInStatement.Left); + } + + _writeContext.ClearNodeProperty(); + Writer.WriteKeyword("in", TokenFlags.InBetween | TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(forInStatement.Right), static node => node.As().Right); + VisitRootExpression(forInStatement.Right, RootExpressionFlags(needsBrackets: false)); + + _writeContext.ClearNodeProperty(); + Writer.WritePunctuator(")", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(forInStatement.Body), static node => node.As().Body); + VisitStatement(forInStatement.Body, StatementBodyFlags(isRightMost: true)); + + return forInStatement; + } + + protected internal override object? VisitForOfStatement(ForOfStatement forOfStatement) + { + Writer.WriteKeyword("for", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + if (forOfStatement.Await) + { + _writeContext.SetNodeProperty(nameof(forOfStatement.Await), static node => node.As().Await); + Writer.WriteKeyword("await", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + } + + Writer.WritePunctuator("(", TokenFlags.Leading | TokenFlags.LeadingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(forOfStatement.Left), static node => node.As().Left); + + if (forOfStatement.Left is VariableDeclaration variableDeclaration) + { + VisitStatement(variableDeclaration, StatementFlags.NestedVariableDeclaration); + } + else + { + VisitAuxiliaryNode(forOfStatement.Left); + } + + _writeContext.ClearNodeProperty(); + Writer.WriteKeyword("of", TokenFlags.InBetween | TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(forOfStatement.Right), static node => node.As().Right); + VisitRootExpression(forOfStatement.Right, RootExpressionFlags(needsBrackets: ExpressionNeedsBracketsInList(forOfStatement.Right))); + + _writeContext.ClearNodeProperty(); + Writer.WritePunctuator(")", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(forOfStatement.Body), static node => node.As().Body); + VisitStatement(forOfStatement.Body, StatementBodyFlags(isRightMost: true)); + + return forOfStatement; + } + + protected internal override object? VisitForStatement(ForStatement forStatement) + { + Writer.WriteKeyword("for", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + Writer.WritePunctuator("(", TokenFlags.Leading | TokenFlags.LeadingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(forStatement.Init), static node => node.As().Init); + + if (forStatement.Init is not null) + { + if (forStatement.Init is VariableDeclaration variableDeclaration) + { + VisitStatement(variableDeclaration, StatementFlags.NestedVariableDeclaration); + } + else + { + VisitRootExpression(forStatement.Init.As(), RootExpressionFlags(needsBrackets: false)); + } + } + + Writer.WritePunctuator(";", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(forStatement.Test), static node => node.As().Test); + + if (forStatement.Test is not null) + { + VisitRootExpression(forStatement.Test, RootExpressionFlags(needsBrackets: false)); + } + + Writer.WritePunctuator(";", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref _writeContext); + + if (forStatement.Update is not null) + { + _writeContext.SetNodeProperty(nameof(forStatement.Update), static node => node.As().Update); + + VisitRootExpression(forStatement.Update, RootExpressionFlags(needsBrackets: false)); + } + + _writeContext.ClearNodeProperty(); + Writer.WritePunctuator(")", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(forStatement.Body), static node => node.As().Body); + VisitStatement(forStatement.Body, StatementBodyFlags(isRightMost: true)); + + return forStatement; + } + + protected internal override object? VisitFunctionDeclaration(FunctionDeclaration functionDeclaration) + { + if (functionDeclaration.Async) + { + _writeContext.SetNodeProperty(nameof(functionDeclaration.Async), static node => node.As().Async); + Writer.WriteKeyword("async", TokenFlags.LeadingSpaceRecommended, ref _writeContext); + + _writeContext.ClearNodeProperty(); + } + + Writer.WriteKeyword("function", TokenFlags.LeadingSpaceRecommended, ref _writeContext); + + if (functionDeclaration.Generator) + { + _writeContext.SetNodeProperty(nameof(functionDeclaration.Generator), static node => node.As().Generator); + Writer.WritePunctuator("*", (functionDeclaration.Id is not null).ToFlag(TokenFlags.TrailingSpaceRecommended), ref _writeContext); + } + + if (functionDeclaration.Id is not null) + { + _writeContext.SetNodeProperty(nameof(functionDeclaration.Id), static node => node.As().Id); + VisitAuxiliaryNode(functionDeclaration.Id); + } + + _writeContext.SetNodeProperty(nameof(functionDeclaration.Params), static node => ref node.As().Params); + Writer.WritePunctuator("(", TokenFlags.Leading, ref _writeContext); + VisitAuxiliaryNodeList(in functionDeclaration.Params, separator: ","); + Writer.WritePunctuator(")", TokenFlags.Trailing, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(functionDeclaration.Body), static node => node.As().Body); + VisitStatement(functionDeclaration.Body, StatementBodyFlags(isRightMost: true)); + + return functionDeclaration; + } + + protected internal override object? VisitFunctionExpression(FunctionExpression functionExpression) + { + if (!_currentExpressionFlags.HasFlagFast(ExpressionFlags.IsMethod)) + { + if (functionExpression.Async) + { + _writeContext.SetNodeProperty(nameof(functionExpression.Async), static node => node.As().Async); + Writer.WriteKeyword("async", ref _writeContext); + + _writeContext.ClearNodeProperty(); + } + + Writer.WriteKeyword("function", ref _writeContext); + + if (functionExpression.Generator) + { + _writeContext.SetNodeProperty(nameof(functionExpression.Generator), static node => node.As().Generator); + Writer.WritePunctuator("*", (functionExpression.Id is not null).ToFlag(TokenFlags.TrailingSpaceRecommended), ref _writeContext); + } + + if (functionExpression.Id is not null) + { + _writeContext.SetNodeProperty(nameof(functionExpression.Id), static node => node.As().Id); + VisitAuxiliaryNode(functionExpression.Id); + } + } + else + { + var keyIsFirstToken = true; + + if (functionExpression.Async) + { + _writeContext.SetNodeProperty(nameof(functionExpression.Async), static node => node.As().Async); + Writer.WriteKeyword("async", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + keyIsFirstToken = false; + } + + if (functionExpression.Generator) + { + _writeContext.SetNodeProperty(nameof(functionExpression.Generator), static node => node.As().Generator); + Writer.WritePunctuator("*", TokenFlags.LeadingSpaceRecommended, ref _writeContext); + + keyIsFirstToken = false; + } + + _writeContext.SetNodeProperty(nameof(functionExpression.Id), static node => node.As().Id); + var property = (IProperty) ParentNode!; + if (property.Kind != PropertyKind.Constructor || property.Key.Type == Nodes.Literal) + { + if (keyIsFirstToken && !property.Computed) + { + Writer.WriteEpsilon(TokenFlags.LeadingSpaceRecommended, ref _writeContext); + } + + VisitPropertyKey(property.Key, property.Computed, leadingBracketFlags: keyIsFirstToken.ToFlag(TokenFlags.LeadingSpaceRecommended)); + } + else + { + Writer.WriteKeyword("constructor", TokenFlags.LeadingSpaceRecommended, ref _writeContext); + } + } + + _writeContext.SetNodeProperty(nameof(functionExpression.Params), static node => ref node.As().Params); + Writer.WritePunctuator("(", TokenFlags.Leading, ref _writeContext); + VisitAuxiliaryNodeList(in functionExpression.Params, separator: ","); + Writer.WritePunctuator(")", TokenFlags.Trailing, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(functionExpression.Body), static node => node.As().Body); + VisitStatement(functionExpression.Body, StatementBodyFlags(isRightMost: true)); + + return functionExpression; + } + + protected internal override object? VisitIdentifier(Identifier identifier) + { + _writeContext.SetNodeProperty(nameof(identifier.Name), static node => node.As().Name); + Writer.WriteIdentifier(identifier.Name!, ref _writeContext); + + return identifier; + } + + protected internal override object? VisitIfStatement(IfStatement ifStatement) + { + Writer.WriteKeyword("if", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(ifStatement.Test), static node => node.As().Test); + VisitRootExpression(ifStatement.Test, ExpressionFlags.SpaceAroundBracketsRecommended | RootExpressionFlags(needsBrackets: true)); + + _writeContext.SetNodeProperty(nameof(ifStatement.Consequent), static node => node.As().Consequent); + StatementFlags bodyFlags; + VisitStatement(ifStatement.Consequent, bodyFlags = StatementBodyFlags(isRightMost: ifStatement.Alternate is null)); + + if (ifStatement.Alternate is not null) + { + _writeContext.ClearNodeProperty(); + Writer.WriteKeyword("else", TokenFlags.SurroundingSpaceRecommended | StatementBodyFlagsToKeywordFlags(bodyFlags), ref _writeContext); + + _writeContext.SetNodeProperty(nameof(ifStatement.Alternate), static node => node.As().Alternate); + VisitStatement(ifStatement.Alternate, StatementBodyFlags(isRightMost: true)); + } + + return ifStatement; + } + + protected internal override object? VisitImport(Import import) + { + Writer.WriteKeyword("import", ref _writeContext); + + Writer.WritePunctuator("(", TokenFlags.Leading, ref _writeContext); + + // Import arguments need special care because of the unusual model (separate expressions instead of an expression list). + + var paramCount = import.Attributes is null ? 1 : 2; + Writer.StartExpressionList(paramCount, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(Import.Source), static node => node.As().Source); + VisitExpressionListItem(import.Source, 0, paramCount, static (@this, expression, _, _) => + s_getCombinedSubExpressionFlags(@this, expression, SubExpressionFlags(@this.ExpressionNeedsBracketsInList(expression), isLeftMost: false))); + + if (import.Attributes is not null) + { + // https://github.com/tc39/proposal-import-assertions + + _writeContext.SetNodeProperty(nameof(Import.Attributes), static node => node.As().Attributes); + VisitExpressionListItem(import.Attributes, 1, paramCount, static (@this, expression, _, _) => + s_getCombinedSubExpressionFlags(@this, expression, SubExpressionFlags(@this.ExpressionNeedsBracketsInList(expression), isLeftMost: false))); + } + + Writer.EndExpressionList(paramCount, ref _writeContext); + + _writeContext.ClearNodeProperty(); + Writer.WritePunctuator(")", TokenFlags.Trailing, ref _writeContext); + + return import; + } + + protected internal override object? VisitImportAttribute(ImportAttribute importAttribute) + { + // https://github.com/tc39/proposal-import-assertions + + _writeContext.SetNodeProperty(nameof(importAttribute.Key), static node => node.As().Key); + VisitPropertyKey(importAttribute.Key, computed: false); + Writer.WritePunctuator(":", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(importAttribute.Value), static node => node.As().Value); + + VisitRootExpression(importAttribute.Value, RootExpressionFlags(needsBrackets: false)); + + return importAttribute; + } + + protected internal override object? VisitImportDeclaration(ImportDeclaration importDeclaration) + { + Writer.WriteKeyword("import", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + // Specifiers need special care because of the unusual syntax. + + _writeContext.SetNodeProperty(nameof(importDeclaration.Specifiers), static node => ref node.As().Specifiers); + Writer.StartAuxiliaryNodeList(importDeclaration.Specifiers.Count, ref _writeContext); + + if (importDeclaration.Specifiers.Count == 0) + { + Writer.EndAuxiliaryNodeList(count: 0, ref _writeContext); + + goto WriteSource; + } + + var index = 0; + Func getNodeContext = static delegate { return null; }; + + if (importDeclaration.Specifiers[index].Type == Nodes.ImportDefaultSpecifier) + { + VisitAuxiliaryNodeListItem(importDeclaration.Specifiers[index], index, importDeclaration.Specifiers.Count, ",", getNodeContext); + + if (++index >= importDeclaration.Specifiers.Count) + { + goto EndSpecifiers; + } + } + + if (importDeclaration.Specifiers[index].Type == Nodes.ImportNamespaceSpecifier) + { + VisitAuxiliaryNodeListItem(importDeclaration.Specifiers[index], index, importDeclaration.Specifiers.Count, ",", getNodeContext); + + if (++index >= importDeclaration.Specifiers.Count) + { + goto EndSpecifiers; + } + } + + Writer.WritePunctuator("{", TokenFlags.Leading | TokenFlags.TrailingSpaceRecommended, ref _writeContext); + + for (; index < importDeclaration.Specifiers.Count; index++) + { + VisitAuxiliaryNodeListItem(importDeclaration.Specifiers[index], index, importDeclaration.Specifiers.Count, ",", getNodeContext); + } + + Writer.WritePunctuator("}", TokenFlags.Trailing | TokenFlags.LeadingSpaceRecommended, ref _writeContext); + +EndSpecifiers: + Writer.EndAuxiliaryNodeList(importDeclaration.Specifiers.Count, ref _writeContext); + + _writeContext.ClearNodeProperty(); + Writer.WriteKeyword("from", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + +WriteSource: + _writeContext.SetNodeProperty(nameof(importDeclaration.Source), static node => node.As().Source); + VisitRootExpression(importDeclaration.Source, RootExpressionFlags(needsBrackets: false)); + + if (importDeclaration.Assertions.Count > 0) + { + _writeContext.SetNodeProperty(nameof(importDeclaration.Assertions), static node => ref node.As().Assertions); + VisitAssertions(in importDeclaration.Assertions); + } + + StatementNeedsSemicolon(); + + return importDeclaration; + } + + protected internal override object? VisitImportDefaultSpecifier(ImportDefaultSpecifier importDefaultSpecifier) + { + _writeContext.SetNodeProperty(nameof(importDefaultSpecifier.Local), static node => node.As().Local); + VisitAuxiliaryNode(importDefaultSpecifier.Local); + + return importDefaultSpecifier; + } + + protected internal override object? VisitImportNamespaceSpecifier(ImportNamespaceSpecifier importNamespaceSpecifier) + { + Writer.WritePunctuator("*", TokenFlags.TrailingSpaceRecommended, ref _writeContext); + + Writer.WriteKeyword("as", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(importNamespaceSpecifier.Local), static node => node.As().Local); + VisitAuxiliaryNode(importNamespaceSpecifier.Local); + + return importNamespaceSpecifier; + } + + protected internal override object? VisitImportSpecifier(ImportSpecifier importSpecifier) + { + if (importSpecifier.Imported != importSpecifier.Local) + { + _writeContext.SetNodeProperty(nameof(importSpecifier.Imported), static node => node.As().Imported); + VisitExportOrImportSpecifierIdentifier(importSpecifier.Imported); + + _writeContext.ClearNodeProperty(); + Writer.WriteKeyword("as", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + } + + _writeContext.SetNodeProperty(nameof(importSpecifier.Local), static node => node.As().Local); + VisitAuxiliaryNode(importSpecifier.Local); + + return importSpecifier; + } + + protected internal override object? VisitLabeledStatement(LabeledStatement labeledStatement) + { + _writeContext.SetNodeProperty(nameof(labeledStatement.Label), static node => node.As().Label); + Writer.WriteEpsilon(TokenFlags.LeadingSpaceRecommended, ref _writeContext); + VisitAuxiliaryNode(labeledStatement.Label); + + Writer.WritePunctuator(":", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(labeledStatement.Body), static node => node.As().Body); + VisitStatement(labeledStatement.Body, StatementFlags.IsRightMost); + + return labeledStatement; + } + + protected internal override object? VisitLiteral(Literal literal) + { + _writeContext.SetNodeProperty(nameof(literal.Raw), static node => node.As().Raw); + Writer.WriteLiteral(literal.Raw, literal.TokenType, ref _writeContext); + + return literal; + } + + protected internal override object? VisitMemberExpression(MemberExpression memberExpression) + { + var operationFlags = BinaryOperandsNeedBrackets(memberExpression, memberExpression.Object, memberExpression.Property); + + // Cases like 1.toString() must be disambiguated with brackets. + if (!operationFlags.HasFlagFast(BinaryOperationFlags.LeftOperandNeedsBrackets) && + memberExpression is { Computed: false, Optional: false, Object: Literal objectLiteral } && + objectLiteral.TokenType == TokenType.NumericLiteral && + objectLiteral.Raw.IndexOf('.') < 0) + { + operationFlags |= BinaryOperationFlags.LeftOperandNeedsBrackets; + } + + _writeContext.SetNodeProperty(nameof(memberExpression.Object), static node => node.As().Object); + VisitSubExpression(memberExpression.Object, SubExpressionFlags(operationFlags.HasFlagFast(BinaryOperationFlags.LeftOperandNeedsBrackets), isLeftMost: true)); + + if (memberExpression.Computed) + { + if (memberExpression.Optional) + { + _writeContext.ClearNodeProperty(); + Writer.WritePunctuator("?.", TokenFlags.InBetween, ref _writeContext); + } + + _writeContext.SetNodeProperty(nameof(memberExpression.Property), static node => node.As().Property); + Writer.WritePunctuator("[", TokenFlags.Leading, ref _writeContext); + VisitSubExpression(memberExpression.Property, SubExpressionFlags(needsBrackets: false, isLeftMost: false)); + Writer.WritePunctuator("]", TokenFlags.Trailing, ref _writeContext); + } + else + { + _writeContext.ClearNodeProperty(); + Writer.WritePunctuator(memberExpression.Optional ? "?." : ".", TokenFlags.InBetween, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(memberExpression.Property), static node => node.As().Property); + VisitSubExpression(memberExpression.Property, SubExpressionFlags(needsBrackets: false, isLeftMost: false)); + } + + return memberExpression; + } + + protected internal override object? VisitMetaProperty(MetaProperty metaProperty) + { + _writeContext.SetNodeProperty(nameof(metaProperty.Meta), static node => node.As().Meta); + Writer.WriteKeyword(metaProperty.Meta.Name!, ref _writeContext); + + _writeContext.ClearNodeProperty(); + Writer.WritePunctuator(".", TokenFlags.InBetween, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(metaProperty.Property), static node => node.As().Property); + VisitSubExpression(metaProperty.Property, SubExpressionFlags(needsBrackets: false, isLeftMost: false)); + + return metaProperty; + } + + protected internal override object? VisitMethodDefinition(MethodDefinition methodDefinition) + { + if (methodDefinition.Decorators.Count > 0) + { + _writeContext.SetNodeProperty(nameof(methodDefinition.Decorators), static node => ref node.As().Decorators); + VisitAuxiliaryNodeList(methodDefinition.Decorators, separator: string.Empty); + + _writeContext.ClearNodeProperty(); + } + + if (methodDefinition.Static) + { + _writeContext.SetNodeProperty(nameof(methodDefinition.Static), static node => node.As().Static); + Writer.WriteKeyword("static", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + } + + switch (methodDefinition.Kind) + { + case PropertyKind.Get: + _writeContext.SetNodeProperty(nameof(methodDefinition.Kind), static node => node.As().Kind); + Writer.WriteKeyword("get", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + break; + case PropertyKind.Set: + _writeContext.SetNodeProperty(nameof(methodDefinition.Kind), static node => node.As().Kind); + Writer.WriteKeyword("set", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + break; + } + + _writeContext.SetNodeProperty(nameof(methodDefinition.Value), static node => node.As().Value); + VisitRootExpression(methodDefinition.Value, ExpressionFlags.IsMethod | RootExpressionFlags(needsBrackets: false)); + + return methodDefinition; + } + + protected internal override object? VisitNewExpression(NewExpression newExpression) + { + Writer.WriteKeyword("new", TokenFlags.TrailingSpaceRecommended, ref _writeContext); + + var calleeNeedsBrackets = UnaryOperandNeedsBrackets(newExpression, newExpression.Callee); + + _writeContext.SetNodeProperty(nameof(newExpression.Callee), static node => node.As().Callee); + VisitExpression(newExpression.Callee, SubExpressionFlags(calleeNeedsBrackets, isLeftMost: false), static (@this, expression, flags) => + @this.DisambiguateExpression(expression, ExpressionFlags.IsInsideNewCallee | ExpressionFlags.IsLeftMostInNewCallee | @this.PropagateExpressionFlags(flags))); + + if (newExpression.Arguments.Count > 0) + { + _writeContext.SetNodeProperty(nameof(newExpression.Arguments), static node => ref node.As().Arguments); + Writer.WritePunctuator("(", TokenFlags.Leading, ref _writeContext); + VisitSubExpressionList(in newExpression.Arguments); + Writer.WritePunctuator(")", TokenFlags.Trailing, ref _writeContext); + } + + return newExpression; + } + + protected internal override object? VisitObjectExpression(ObjectExpression objectExpression) + { + _writeContext.SetNodeProperty(nameof(objectExpression.Properties), static node => ref node.As().Properties); + + Writer.StartObject(objectExpression.Properties.Count, ref _writeContext); + + // Properties need special care because it may contain spread elements, which are actual expressions (as opposed to normal properties). + + Writer.StartAuxiliaryNodeList(objectExpression.Properties.Count, ref _writeContext); + + for (var i = 0; i < objectExpression.Properties.Count; i++) + { + var property = objectExpression.Properties[i]; + if (property is SpreadElement spreadElement) + { + var originalAuxiliaryNodeContext = _currentAuxiliaryNodeContext; + _currentAuxiliaryNodeContext = null; + + Writer.StartAuxiliaryNodeListItem(i, objectExpression.Properties.Count, separator: ",", _currentAuxiliaryNodeContext, ref _writeContext); + VisitRootExpression(spreadElement, RootExpressionFlags(needsBrackets: ExpressionNeedsBracketsInList(spreadElement))); + Writer.EndAuxiliaryNodeListItem(i, objectExpression.Properties.Count, separator: ",", _currentAuxiliaryNodeContext, ref _writeContext); + + _currentAuxiliaryNodeContext = originalAuxiliaryNodeContext; + } + else + { + VisitAuxiliaryNodeListItem(property, i, objectExpression.Properties.Count, separator: ",", static delegate { return null; }); + } + } + + Writer.EndAuxiliaryNodeList(objectExpression.Properties.Count, ref _writeContext); + + Writer.EndObject(objectExpression.Properties.Count, ref _writeContext); + + return objectExpression; + } + + protected internal override object? VisitObjectPattern(ObjectPattern objectPattern) + { + _writeContext.SetNodeProperty(nameof(objectPattern.Properties), static node => ref node.As().Properties); + + Writer.StartObject(objectPattern.Properties.Count, ref _writeContext); + + VisitAuxiliaryNodeList(in objectPattern.Properties, separator: ","); + + Writer.EndObject(objectPattern.Properties.Count, ref _writeContext); + + return objectPattern; + } + + protected internal override object? VisitPrivateIdentifier(PrivateIdentifier privateIdentifier) + { + _writeContext.SetNodeProperty(nameof(privateIdentifier.Name), static node => node.As().Name); + Writer.WritePunctuator("#", TokenFlags.Leading, ref _writeContext); + Writer.WriteIdentifier(privateIdentifier.Name!, ref _writeContext); + + return privateIdentifier; + } + + protected internal override object? VisitProgram(Program program) + { + _writeContext.SetNodeProperty(nameof(program.Body), static node => ref node.As().Body); + VisitStatementList(in program.Body); + + return program; + } + + protected internal override object? VisitProperty(Property property) + { + bool isMethod; + + switch (property.Kind) + { + case PropertyKind.Get: + _writeContext.SetNodeProperty(nameof(property.Kind), static node => node.As().Kind); + Writer.WriteKeyword("get", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + isMethod = true; + break; + case PropertyKind.Set: + _writeContext.SetNodeProperty(nameof(property.Kind), static node => node.As().Kind); + Writer.WriteKeyword("set", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + isMethod = true; + break; + case PropertyKind.Init when property.Method: + isMethod = true; + break; + default: + if (!property.Shorthand) + { + _writeContext.SetNodeProperty(nameof(property.Key), static node => node.As().Key); + VisitPropertyKey(property.Key, property.Computed, leadingBracketFlags: TokenFlags.LeadingSpaceRecommended); + Writer.WritePunctuator(":", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref _writeContext); + } + + isMethod = false; + break; + } + + _writeContext.SetNodeProperty(nameof(property.Value), static node => node.As().Value); + + if (ParentNode is { Type: Nodes.ObjectPattern }) + { + VisitAuxiliaryNode(property.Value); + } + else + { + var expression = property.Value.As(); + VisitRootExpression(expression, isMethod.ToFlag(ExpressionFlags.IsMethod) | RootExpressionFlags(needsBrackets: ExpressionNeedsBracketsInList(expression))); + } + + return property; + } + + protected internal override object? VisitPropertyDefinition(PropertyDefinition propertyDefinition) + { + if (propertyDefinition.Decorators.Count > 0) + { + _writeContext.SetNodeProperty(nameof(propertyDefinition.Decorators), static node => ref node.As().Decorators); + VisitAuxiliaryNodeList(propertyDefinition.Decorators, separator: string.Empty); + + _writeContext.ClearNodeProperty(); + } + + if (propertyDefinition.Static) + { + _writeContext.SetNodeProperty(nameof(propertyDefinition.Static), static node => node.As().Static); + Writer.WriteKeyword("static", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + } + + _writeContext.SetNodeProperty(nameof(propertyDefinition.Key), static node => node.As().Key); + VisitPropertyKey(propertyDefinition.Key, propertyDefinition.Computed, leadingBracketFlags: TokenFlags.LeadingSpaceRecommended); + + if (propertyDefinition.Value is not null) + { + _writeContext.ClearNodeProperty(); + Writer.WritePunctuator("=", TokenFlags.InBetween | TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(propertyDefinition.Value), static node => node.As().Value); + VisitRootExpression(propertyDefinition.Value, RootExpressionFlags(needsBrackets: ExpressionNeedsBracketsInList(propertyDefinition.Value))); + } + + Writer.WritePunctuator(";", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref _writeContext); + + return propertyDefinition; + } + + protected internal override object? VisitRestElement(RestElement restElement) + { + _writeContext.SetNodeProperty(nameof(restElement.Argument), static node => node.As().Argument); + Writer.WritePunctuator("...", TokenFlags.Leading, ref _writeContext); + + VisitAuxiliaryNode(restElement.Argument); + + return restElement; + } + + protected internal override object? VisitReturnStatement(ReturnStatement returnStatement) + { + Writer.WriteKeyword("return", (returnStatement.Argument is not null).ToFlag(TokenFlags.SurroundingSpaceRecommended, TokenFlags.LeadingSpaceRecommended), ref _writeContext); + + if (returnStatement.Argument is not null) + { + _writeContext.SetNodeProperty(nameof(returnStatement.Argument), static node => node.As().Argument); + VisitRootExpression(returnStatement.Argument, RootExpressionFlags(needsBrackets: false)); + } + + StatementNeedsSemicolon(); + + return returnStatement; + } + + protected internal override object? VisitSequenceExpression(SequenceExpression sequenceExpression) + { + _writeContext.SetNodeProperty(nameof(sequenceExpression.Expressions), static node => ref node.As().Expressions); + + VisitExpressionList(in sequenceExpression.Expressions, static (@this, expression, index, _) => + s_getCombinedSubExpressionFlags(@this, expression, SubExpressionFlags(@this.ExpressionNeedsBracketsInList(expression), isLeftMost: index == 0))); + + return sequenceExpression; + } + + protected internal override object? VisitSpreadElement(SpreadElement spreadElement) + { + var argumentNeedsBrackets = UnaryOperandNeedsBrackets(spreadElement, spreadElement.Argument); + + _writeContext.SetNodeProperty(nameof(spreadElement.Argument), static node => node.As().Argument); + Writer.WritePunctuator("...", TokenFlags.Leading, ref _writeContext); + + VisitSubExpression(spreadElement.Argument, SubExpressionFlags(argumentNeedsBrackets, isLeftMost: false)); + + return spreadElement; + } + + protected internal override object? VisitStaticBlock(StaticBlock staticBlock) + { + Writer.WriteKeyword("static", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(staticBlock.Body), static node => ref node.As().Body); + Writer.StartBlock(staticBlock.Body.Count, ref _writeContext); + + VisitStatementList(in staticBlock.Body); + + Writer.EndBlock(staticBlock.Body.Count, ref _writeContext); + + return staticBlock; + } + + protected internal override object? VisitSuper(Super super) + { + Writer.WriteKeyword("super", ref _writeContext); + + return super; + } + + protected internal override object? VisitSwitchCase(SwitchCase switchCase) + { + if (switchCase.Test is not null) + { + Writer.WriteKeyword("case", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(switchCase.Test), static node => node.As().Test); + VisitRootExpression(switchCase.Test, RootExpressionFlags(needsBrackets: false)); + + _writeContext.ClearNodeProperty(); + } + else + { + Writer.WriteKeyword("default", TokenFlags.LeadingSpaceRecommended, ref _writeContext); + } + + Writer.WritePunctuator(":", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(switchCase.Consequent), static node => ref node.As().Consequent); + + if (_currentAuxiliaryNodeContext == s_lastSwitchCaseFlag) + { + // If this is the last case, then the right-most semicolon can be omitted. + VisitStatementList(in switchCase.Consequent); + } + else + { + // If this isn't the last case, then the right-most semicolon must not be omitted! + VisitStatementList(in switchCase.Consequent, static delegate { return StatementFlags.None; }); + } + + return switchCase; + } + + protected internal override object? VisitSwitchStatement(SwitchStatement switchStatement) + { + Writer.WriteKeyword("switch", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(switchStatement.Discriminant), static node => node.As().Discriminant); + VisitRootExpression(switchStatement.Discriminant, ExpressionFlags.SpaceAroundBracketsRecommended | RootExpressionFlags(needsBrackets: true)); + + _writeContext.SetNodeProperty(nameof(switchStatement.Cases), static node => ref node.As().Cases); + Writer.StartBlock(switchStatement.Cases.Count, ref _writeContext); + + // Passes contextual information about whether it's the last one in the statement or not to each SwitchCase. + VisitAuxiliaryNodeList(in switchStatement.Cases, separator: string.Empty, static (_, _, index, count) => + index == count - 1 ? s_lastSwitchCaseFlag : null); + + Writer.EndBlock(switchStatement.Cases.Count, ref _writeContext); + + return switchStatement; + } + + protected internal override object? VisitTaggedTemplateExpression(TaggedTemplateExpression taggedTemplateExpression) + { + _writeContext.SetNodeProperty(nameof(taggedTemplateExpression.Tag), static node => node.As().Tag); + VisitExpression(taggedTemplateExpression.Tag, SubExpressionFlags(needsBrackets: false, isLeftMost: true), static (@this, expression, flags) => + @this.DisambiguateExpression(expression, ExpressionFlags.IsInsideLeftHandSideExpression | ExpressionFlags.IsLeftMostInLeftHandSideExpression | @this.PropagateExpressionFlags(flags))); + + _writeContext.SetNodeProperty(nameof(taggedTemplateExpression.Quasi), static node => node.As().Quasi); + VisitSubExpression(taggedTemplateExpression.Quasi, SubExpressionFlags(needsBrackets: false, isLeftMost: false)); + + return taggedTemplateExpression; + } + + protected internal override object? VisitTemplateElement(TemplateElement templateElement) + { + _writeContext.SetNodeProperty(nameof(templateElement.Value), static node => node.As().Value); + Writer.WriteLiteral(templateElement.Value.Raw, TokenType.Template, ref _writeContext); + + return templateElement; + } + + protected internal override object? VisitTemplateLiteral(TemplateLiteral templateLiteral) + { + Writer.WritePunctuator("`", TokenFlags.Leading, ref _writeContext); + + TemplateElement quasi; + for (var i = 0; !(quasi = templateLiteral.Quasis[i]).Tail; i++) + { + _writeContext.SetNodeProperty(nameof(templateLiteral.Quasis), static node => ref node.As().Quasis); + VisitAuxiliaryNode(quasi); + + _writeContext.SetNodeProperty(nameof(templateLiteral.Expressions), static node => ref node.As().Expressions); + Writer.WritePunctuator("${", TokenFlags.Leading, ref _writeContext); + VisitRootExpression(templateLiteral.Expressions[i], RootExpressionFlags(needsBrackets: false)); + Writer.WritePunctuator("}", TokenFlags.Trailing, ref _writeContext); + } + + _writeContext.SetNodeProperty(nameof(templateLiteral.Quasis), static node => ref node.As().Quasis); + VisitAuxiliaryNode(quasi); + + Writer.WritePunctuator("`", TokenFlags.Trailing, ref _writeContext); + + return templateLiteral; + } + + protected internal override object? VisitThisExpression(ThisExpression thisExpression) + { + Writer.WriteKeyword("this", ref _writeContext); + + return thisExpression; + } + + protected internal override object? VisitThrowStatement(ThrowStatement throwStatement) + { + Writer.WriteKeyword("throw", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(throwStatement.Argument), static node => node.As().Argument); + VisitRootExpression(throwStatement.Argument, RootExpressionFlags(needsBrackets: false)); + + StatementNeedsSemicolon(); + + return throwStatement; + } + + protected internal override object? VisitTryStatement(TryStatement tryStatement) + { + Writer.WriteKeyword("try", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(tryStatement.Block), static node => node.As().Block); + StatementFlags bodyFlags; + VisitStatement(tryStatement.Block, bodyFlags = StatementBodyFlags(isRightMost: false)); + + if (tryStatement.Handler is not null) + { + _writeContext.ClearNodeProperty(); + Writer.WriteKeyword("catch", TokenFlags.SurroundingSpaceRecommended | StatementBodyFlagsToKeywordFlags(bodyFlags), ref _writeContext); + + _writeContext.SetNodeProperty(nameof(tryStatement.Handler), static node => node.As().Handler); + VisitAuxiliaryNode(tryStatement.Handler); + bodyFlags = StatementBodyFlags(isRightMost: tryStatement.Finalizer is null); + } + + if (tryStatement.Finalizer is not null) + { + _writeContext.ClearNodeProperty(); + Writer.WriteKeyword("finally", TokenFlags.SurroundingSpaceRecommended | StatementBodyFlagsToKeywordFlags(bodyFlags), ref _writeContext); + + _writeContext.SetNodeProperty(nameof(tryStatement.Finalizer), static node => node.As().Finalizer); + VisitStatement(tryStatement.Finalizer, StatementBodyFlags(isRightMost: true)); + } + + return tryStatement; + } + + protected internal override object? VisitUnaryExpression(UnaryExpression unaryExpression) + { + var argumentNeedsBrackets = UnaryOperandNeedsBrackets(unaryExpression, unaryExpression.Argument); + var op = UnaryExpression.GetUnaryOperatorToken(unaryExpression.Operator); + + if (unaryExpression.Prefix) + { + _writeContext.SetNodeProperty(nameof(unaryExpression.Operator), static node => node.As().Operator); + if (char.IsLetter(op[0])) + { + Writer.WriteKeyword(op, TokenFlags.TrailingSpaceRecommended, ref _writeContext); + } + else + { + Writer.WritePunctuator(op, TokenFlags.Leading, ref _writeContext); + + // Cases like +(+x) or +(++x) must be disambiguated with brackets. + if (!argumentNeedsBrackets && + unaryExpression.Argument is UnaryExpression argumentUnaryExpression && + argumentUnaryExpression.Prefix && + op[op.Length - 1] is '+' or '-' && + op[op.Length - 1] == UnaryExpression.GetUnaryOperatorToken(argumentUnaryExpression.Operator)[0]) + { + argumentNeedsBrackets = true; + } + } + + _writeContext.SetNodeProperty(nameof(unaryExpression.Argument), static node => node.As().Argument); + VisitSubExpression(unaryExpression.Argument, SubExpressionFlags(argumentNeedsBrackets, isLeftMost: false)); + } + else + { + _writeContext.SetNodeProperty(nameof(unaryExpression.Argument), static node => node.As().Argument); + VisitSubExpression(unaryExpression.Argument, SubExpressionFlags(argumentNeedsBrackets, isLeftMost: true)); + + _writeContext.SetNodeProperty(nameof(unaryExpression.Operator), static node => node.As().Operator); + Writer.WritePunctuator(op, TokenFlags.Trailing, ref _writeContext); + } + + return unaryExpression; + } + + protected internal override object? VisitVariableDeclaration(VariableDeclaration variableDeclaration) + { + _writeContext.SetNodeProperty(nameof(variableDeclaration.Kind), static node => node.As().Kind); + Writer.WriteKeyword(VariableDeclaration.GetVariableDeclarationKindToken(variableDeclaration.Kind), + _currentStatementFlags.HasFlagFast(StatementFlags.NestedVariableDeclaration).ToFlag(TokenFlags.TrailingSpaceRecommended, TokenFlags.SurroundingSpaceRecommended), ref _writeContext); + + _writeContext.SetNodeProperty(nameof(variableDeclaration.Declarations), static node => ref node.As().Declarations); + + if (!_currentStatementFlags.HasFlagFast(StatementFlags.NestedVariableDeclaration)) + { + VisitAuxiliaryNodeList(in variableDeclaration.Declarations, separator: ","); + + StatementNeedsSemicolon(); + } + else if (ParentNode is not { Type: Nodes.ForStatement or Nodes.ForInStatement }) + { + VisitAuxiliaryNodeList(in variableDeclaration.Declarations, separator: ","); + } + else + { + VisitAuxiliaryNodeList(in variableDeclaration.Declarations, separator: ",", static delegate { return s_forLoopInitDeclarationFlag; }); + } + + return variableDeclaration; + } + + protected internal override object? VisitVariableDeclarator(VariableDeclarator variableDeclarator) + { + _writeContext.SetNodeProperty(nameof(variableDeclarator.Id), static node => node.As().Id); + VisitAuxiliaryNode(variableDeclarator.Id); + + if (variableDeclarator.Init is not null) + { + _writeContext.ClearNodeProperty(); + Writer.WritePunctuator("=", TokenFlags.InBetween | TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(variableDeclarator.Init), static node => node.As().Init); + + if (_currentAuxiliaryNodeContext != s_forLoopInitDeclarationFlag) + { + VisitRootExpression(variableDeclarator.Init, RootExpressionFlags(needsBrackets: ExpressionNeedsBracketsInList(variableDeclarator.Init))); + } + else + { + VisitRootExpression(variableDeclarator.Init, ExpressionFlags.InOperatorIsAmbiguousInDeclaration | RootExpressionFlags(needsBrackets: ExpressionNeedsBracketsInList(variableDeclarator.Init))); + } + } + + return variableDeclarator; + } + + protected internal override object? VisitWhileStatement(WhileStatement whileStatement) + { + Writer.WriteKeyword("while", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(whileStatement.Test), static node => node.As().Test); + VisitRootExpression(whileStatement.Test, ExpressionFlags.SpaceAroundBracketsRecommended | RootExpressionFlags(needsBrackets: true)); + + _writeContext.SetNodeProperty(nameof(whileStatement.Body), static node => node.As().Body); + VisitStatement(whileStatement.Body, StatementBodyFlags(isRightMost: true)); + + return whileStatement; + } + + protected internal override object? VisitWithStatement(WithStatement withStatement) + { + Writer.WriteKeyword("with", TokenFlags.SurroundingSpaceRecommended, ref _writeContext); + + _writeContext.SetNodeProperty(nameof(withStatement.Object), static node => node.As().Object); + VisitRootExpression(withStatement.Object, ExpressionFlags.SpaceAroundBracketsRecommended | RootExpressionFlags(needsBrackets: true)); + + _writeContext.SetNodeProperty(nameof(withStatement.Body), static node => node.As().Body); + VisitStatement(withStatement.Body, StatementBodyFlags(isRightMost: true)); + + return withStatement; + } + + protected internal override object? VisitYieldExpression(YieldExpression yieldExpression) + { + Writer.WriteKeyword("yield", (!yieldExpression.Delegate && yieldExpression.Argument is not null).ToFlag(TokenFlags.TrailingSpaceRecommended), ref _writeContext); + + if (yieldExpression.Delegate) + { + _writeContext.SetNodeProperty(nameof(yieldExpression.Delegate), static node => node.As().Delegate); + Writer.WritePunctuator("*", (yieldExpression.Argument is not null).ToFlag(TokenFlags.TrailingSpaceRecommended), ref _writeContext); + } + + if (yieldExpression.Argument is not null) + { + var argumentNeedsBrackets = UnaryOperandNeedsBrackets(yieldExpression, yieldExpression.Argument); + + _writeContext.SetNodeProperty(nameof(yieldExpression.Argument), static node => node.As().Argument); + VisitSubExpression(yieldExpression.Argument, SubExpressionFlags(argumentNeedsBrackets, isLeftMost: false)); + } + + return yieldExpression; + } +} diff --git a/src/Esprima/Utils/AstToJson.cs b/src/Esprima/Utils/AstToJson.cs new file mode 100644 index 00000000..cbb090d9 --- /dev/null +++ b/src/Esprima/Utils/AstToJson.cs @@ -0,0 +1,88 @@ +using Esprima.Ast; + +namespace Esprima.Utils; + +public enum LocationMembersPlacement +{ + End, + Start +} + +internal enum AstToJsonTestCompatibilityMode +{ + None, + EsprimaOrg, +} + +public record class AstToJsonOptions +{ + public static readonly AstToJsonOptions Default = new(); + + public bool IncludingLineColumn { get; init; } + public bool IncludingRange { get; init; } + public LocationMembersPlacement LocationMembersPlacement { get; init; } + /// + /// This switch is intended for enabling a compatibility mode for to build a JSON output + /// which matches the format of the test fixtures of the original Esprima project. + /// + internal AstToJsonTestCompatibilityMode TestCompatibilityMode { get; init; } + + protected internal virtual AstToJsonConverter CreateConverter(JsonWriter writer) => new AstToJsonConverter(writer, this); +} + +public static class AstToJson +{ + public static string ToJsonString(this Node node) + { + return ToJsonString(node, indent: null); + } + + public static string ToJsonString(this Node node, string? indent) + { + return ToJsonString(node, AstToJsonOptions.Default, indent); + } + + public static string ToJsonString(this Node node, AstToJsonOptions options) + { + return ToJsonString(node, options, indent: null); + } + + public static string ToJsonString(this Node node, AstToJsonOptions options, string? indent) + { + using (var writer = new StringWriter()) + { + WriteJson(node, writer, options, indent); + return writer.ToString(); + } + } + + public static void WriteJson(this Node node, TextWriter writer) + { + WriteJson(node, writer, indent: null); + } + + public static void WriteJson(this Node node, TextWriter writer, string? indent) + { + WriteJson(node, writer, AstToJsonOptions.Default, indent); + } + + public static void WriteJson(this Node node, TextWriter writer, AstToJsonOptions options) + { + WriteJson(node, writer, options, indent: null); + } + + public static void WriteJson(this Node node, TextWriter writer, AstToJsonOptions options, string? indent) + { + WriteJson(node, new JsonTextWriter(writer, indent), options); + } + + public static void WriteJson(this Node node, JsonWriter writer, AstToJsonOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.CreateConverter(writer).Convert(node); + } +} diff --git a/src/Esprima/Utils/AstToJsonConverter.cs b/src/Esprima/Utils/AstToJsonConverter.cs new file mode 100644 index 00000000..1e00aa21 --- /dev/null +++ b/src/Esprima/Utils/AstToJsonConverter.cs @@ -0,0 +1,1132 @@ +using System.Collections; +using System.Globalization; +using System.Numerics; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Esprima.Ast; + +namespace Esprima.Utils; + +public class AstToJsonConverter : AstVisitor +{ + private readonly JsonWriter _writer; + private protected readonly bool _includeLineColumn; + private protected readonly bool _includeRange; + private protected readonly LocationMembersPlacement _locationMembersPlacement; + private protected readonly AstToJsonTestCompatibilityMode _testCompatibilityMode; + + public AstToJsonConverter(JsonWriter writer, AstToJsonOptions options) + { + _writer = writer ?? throw new ArgumentNullException(nameof(writer)); + + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + _includeLineColumn = options.IncludingLineColumn; + _includeRange = options.IncludingRange; + _locationMembersPlacement = options.LocationMembersPlacement; + _testCompatibilityMode = options.TestCompatibilityMode; + } + + protected virtual string GetNodeType(Node node) + { + return node.Type.ToString(); + } + + private void WriteLocationInfo(Node node) + { + if (node is ChainExpression) + { + return; + } + + if (_includeRange) + { + _writer.Member("range"); + _writer.StartArray(); + _writer.Number(node.Range.Start); + _writer.Number(node.Range.End); + _writer.EndArray(); + } + + if (_includeLineColumn) + { + _writer.Member("loc"); + _writer.StartObject(); + _writer.Member("start"); + Write(node.Location.Start); + _writer.Member("end"); + Write(node.Location.End); + _writer.EndObject(); + } + + void Write(Position position) + { + _writer.StartObject(); + Member("line", position.Line); + Member("column", position.Column); + _writer.EndObject(); + } + } + + private void OnStartNodeObject(Node node) + { + _writer.StartObject(); + + if ((_includeLineColumn || _includeRange) + && _locationMembersPlacement == LocationMembersPlacement.Start) + { + WriteLocationInfo(node); + } + + Member("type", GetNodeType(node)); + } + + private void OnFinishNodeObject(Node node) + { + if ((_includeLineColumn || _includeRange) + && _locationMembersPlacement == LocationMembersPlacement.End) + { + WriteLocationInfo(node); + } + + _writer.EndObject(); + } + + protected readonly struct NodeObjectDisposable : IDisposable + { + private readonly AstToJsonConverter _converter; + private readonly Node _node; + + public NodeObjectDisposable(AstToJsonConverter converter, Node node) + { + _converter = converter; + _node = node; + } + + public void Dispose() + { + _converter.OnFinishNodeObject(_node); + } + } + + protected NodeObjectDisposable StartNodeObject(Node node) + { + OnStartNodeObject(node); + return new NodeObjectDisposable(this, node); + } + + protected void EmptyNodeObject(Node node) + { + using (StartNodeObject(node)) { } + } + + protected void Member(string name) + { + _writer.Member(name); + } + + protected void Member(string name, Node? node) + { + Member(name); + Visit(node); + } + + protected void Member(string name, string? value) + { + Member(name); + _writer.String(value); + } + + protected void Member(string name, bool value) + { + Member(name); + _writer.Boolean(value); + } + + protected void Member(string name, int value) + { + Member(name); + _writer.Number(value); + } + + private static readonly ConditionalWeakTable EnumMap = new(); + + protected void Member(string name, T value) where T : Enum + { + var map = (Dictionary) + EnumMap.GetValue(value.GetType(), + t => t.GetRuntimeFields() + .Where(f => f.IsStatic) + .ToDictionary(f => (T) f.GetValue(null), f => f.Name.ToLowerInvariant())); + Member(name, map[value]); + } + + protected void Member(string name, in NodeList nodes) where T : Node? + { + Member(name, nodes, node => node); + } + + protected void Member(string name, in NodeList list, Func nodeSelector) where T : Node? + { + Member(name); + _writer.StartArray(); + foreach (var item in list) + { + Visit(nodeSelector(item)); + } + + _writer.EndArray(); + } + + public void Convert(Node node) + { + Visit(node ?? throw new ArgumentNullException(nameof(node))); + } + + public override object? Visit(Node? node) + { + if (node is not null) + { + return base.Visit(node); + } + else + { + _writer.Null(); + return node!; + } + } + + protected internal override object? VisitArrayExpression(ArrayExpression arrayExpression) + { + using (StartNodeObject(arrayExpression)) + { + Member("elements", arrayExpression.Elements); + } + + return arrayExpression; + } + + protected internal override object? VisitArrayPattern(ArrayPattern arrayPattern) + { + using (StartNodeObject(arrayPattern)) + { + Member("elements", arrayPattern.Elements); + } + + return arrayPattern; + } + + protected internal override object? VisitArrowFunctionExpression(ArrowFunctionExpression arrowFunctionExpression) + { + using (StartNodeObject(arrowFunctionExpression)) + { + Member("id", ((IFunction) arrowFunctionExpression).Id); + Member("params", arrowFunctionExpression.Params); + Member("body", arrowFunctionExpression.Body); + Member("generator", ((IFunction) arrowFunctionExpression).Generator); + Member("expression", arrowFunctionExpression.Expression); + // original Esprima doesn't include this information yet + if (_testCompatibilityMode != AstToJsonTestCompatibilityMode.EsprimaOrg) + { + Member("strict", arrowFunctionExpression.Strict); + } + Member("async", arrowFunctionExpression.Async); + } + + return arrowFunctionExpression; + } + + protected internal override object? VisitAssignmentExpression(AssignmentExpression assignmentExpression) + { + using (StartNodeObject(assignmentExpression)) + { + Member("operator", AssignmentExpression.GetAssignmentOperatorToken(assignmentExpression.Operator)); + Member("left", assignmentExpression.Left); + Member("right", assignmentExpression.Right); + } + + return assignmentExpression; + } + + protected internal override object? VisitAssignmentPattern(AssignmentPattern assignmentPattern) + { + using (StartNodeObject(assignmentPattern)) + { + Member("left", assignmentPattern.Left); + Member("right", assignmentPattern.Right); + } + + return assignmentPattern; + } + + protected internal override object? VisitAwaitExpression(AwaitExpression awaitExpression) + { + using (StartNodeObject(awaitExpression)) + { + Member("argument", awaitExpression.Argument); + } + + return awaitExpression; + } + + protected internal override object? VisitBinaryExpression(BinaryExpression binaryExpression) + { + using (StartNodeObject(binaryExpression)) + { + Member("operator", BinaryExpression.GetBinaryOperatorToken(binaryExpression.Operator)); + Member("left", binaryExpression.Left); + Member("right", binaryExpression.Right); + } + + return binaryExpression; + } + + protected internal override object? VisitBlockStatement(BlockStatement blockStatement) + { + using (StartNodeObject(blockStatement)) + { + Member("body", blockStatement.Body, e => (Statement) e); + } + + return blockStatement; + } + + protected internal override object? VisitBreakStatement(BreakStatement breakStatement) + { + using (StartNodeObject(breakStatement)) + { + Member("label", breakStatement.Label); + } + + return breakStatement; + } + + protected internal override object? VisitCallExpression(CallExpression callExpression) + { + using (StartNodeObject(callExpression)) + { + Member("callee", callExpression.Callee); + Member("arguments", callExpression.Arguments, e => e); + Member("optional", callExpression.Optional); + } + + return callExpression; + } + + protected internal override object? VisitCatchClause(CatchClause catchClause) + { + using (StartNodeObject(catchClause)) + { + Member("param", catchClause.Param); + Member("body", catchClause.Body); + } + + return catchClause; + } + + protected internal override object? VisitChainExpression(ChainExpression chainExpression) + { + using (StartNodeObject(chainExpression)) + { + Member("expression", chainExpression.Expression); + } + + return chainExpression; + } + + protected internal override object? VisitClassBody(ClassBody classBody) + { + using (StartNodeObject(classBody)) + { + Member("body", classBody.Body); + } + + return classBody; + } + + protected internal override object? VisitClassDeclaration(ClassDeclaration classDeclaration) + { + using (StartNodeObject(classDeclaration)) + { + Member("id", classDeclaration.Id); + Member("superClass", classDeclaration.SuperClass); + Member("body", classDeclaration.Body); + if (classDeclaration.Decorators.Count > 0) + { + Member("decorators", classDeclaration.Decorators); + } + } + + return classDeclaration; + } + + protected internal override object? VisitClassExpression(ClassExpression classExpression) + { + using (StartNodeObject(classExpression)) + { + Member("id", classExpression.Id); + Member("superClass", classExpression.SuperClass); + Member("body", classExpression.Body); + if (classExpression.Decorators.Count > 0) + { + Member("decorators", classExpression.Decorators); + } + } + + return classExpression; + } + + protected internal override object? VisitConditionalExpression(ConditionalExpression conditionalExpression) + { + using (StartNodeObject(conditionalExpression)) + { + Member("test", conditionalExpression.Test); + Member("consequent", conditionalExpression.Consequent); + Member("alternate", conditionalExpression.Alternate); + } + + return conditionalExpression; + } + + protected internal override object? VisitContinueStatement(ContinueStatement continueStatement) + { + using (StartNodeObject(continueStatement)) + { + Member("label", continueStatement.Label); + } + + return continueStatement; + } + + protected internal override object? VisitDebuggerStatement(DebuggerStatement debuggerStatement) + { + EmptyNodeObject(debuggerStatement); + return debuggerStatement; + } + + protected internal override object? VisitDecorator(Decorator decorator) + { + using (StartNodeObject(decorator)) + { + Member("expression", decorator.Expression); + } + + return decorator; + } + + protected internal override object? VisitDoWhileStatement(DoWhileStatement doWhileStatement) + { + using (StartNodeObject(doWhileStatement)) + { + Member("body", doWhileStatement.Body); + Member("test", doWhileStatement.Test); + } + + return doWhileStatement; + } + + protected internal override object? VisitEmptyStatement(EmptyStatement emptyStatement) + { + EmptyNodeObject(emptyStatement); + return emptyStatement; + } + + protected internal override object? VisitExportAllDeclaration(ExportAllDeclaration exportAllDeclaration) + { + using (StartNodeObject(exportAllDeclaration)) + { + Member("source", exportAllDeclaration.Source); + + // original Esprima doesn't include this information yet + if (_testCompatibilityMode != AstToJsonTestCompatibilityMode.EsprimaOrg) + { + Member("exported", exportAllDeclaration.Exported); + if (exportAllDeclaration.Assertions.Count > 0) + { + Member("assertions", exportAllDeclaration.Assertions); + } + } + } + + return exportAllDeclaration; + } + + protected internal override object? VisitExportDefaultDeclaration(ExportDefaultDeclaration exportDefaultDeclaration) + { + using (StartNodeObject(exportDefaultDeclaration)) + { + Member("declaration", exportDefaultDeclaration.Declaration); + } + + return exportDefaultDeclaration; + } + + protected internal override object? VisitExportNamedDeclaration(ExportNamedDeclaration exportNamedDeclaration) + { + using (StartNodeObject(exportNamedDeclaration)) + { + Member("declaration", exportNamedDeclaration.Declaration); + Member("specifiers", exportNamedDeclaration.Specifiers); + Member("source", exportNamedDeclaration.Source); + // original Esprima doesn't include this information yet + if (_testCompatibilityMode != AstToJsonTestCompatibilityMode.EsprimaOrg && exportNamedDeclaration.Assertions.Count > 0) + { + Member("assertions", exportNamedDeclaration.Assertions); + } + } + + return exportNamedDeclaration; + } + + protected internal override object? VisitExportSpecifier(ExportSpecifier exportSpecifier) + { + using (StartNodeObject(exportSpecifier)) + { + Member("exported", exportSpecifier.Exported); + Member("local", exportSpecifier.Local); + } + + return exportSpecifier; + } + + protected internal override object? VisitExpressionStatement(ExpressionStatement expressionStatement) + { + using (StartNodeObject(expressionStatement)) + { + if (expressionStatement is Directive d) + { + Member("directive", d.Directiv); + } + + Member("expression", expressionStatement.Expression); + } + + return expressionStatement; + } + + protected internal override object? VisitForInStatement(ForInStatement forInStatement) + { + using (StartNodeObject(forInStatement)) + { + Member("left", forInStatement.Left); + Member("right", forInStatement.Right); + Member("body", forInStatement.Body); + Member("each", false); + } + + return forInStatement; + } + + protected internal override object? VisitForOfStatement(ForOfStatement forOfStatement) + { + using (StartNodeObject(forOfStatement)) + { + Member("await", forOfStatement.Await); + Member("left", forOfStatement.Left); + Member("right", forOfStatement.Right); + Member("body", forOfStatement.Body); + } + + return forOfStatement; + } + + protected internal override object? VisitForStatement(ForStatement forStatement) + { + using (StartNodeObject(forStatement)) + { + Member("init", forStatement.Init); + Member("test", forStatement.Test); + Member("update", forStatement.Update); + Member("body", forStatement.Body); + } + + return forStatement; + } + + protected internal override object? VisitFunctionDeclaration(FunctionDeclaration functionDeclaration) + { + using (StartNodeObject(functionDeclaration)) + { + Member("id", functionDeclaration.Id); + Member("params", functionDeclaration.Params); + Member("body", functionDeclaration.Body); + Member("generator", functionDeclaration.Generator); + Member("expression", ((IFunction) functionDeclaration).Expression); + // original Esprima doesn't include this information yet + if (_testCompatibilityMode != AstToJsonTestCompatibilityMode.EsprimaOrg) + { + Member("strict", functionDeclaration.Strict); + } + Member("async", functionDeclaration.Async); + } + + return functionDeclaration; + } + + protected internal override object? VisitFunctionExpression(FunctionExpression functionExpression) + { + using (StartNodeObject(functionExpression)) + { + Member("id", functionExpression.Id); + Member("params", functionExpression.Params); + Member("body", functionExpression.Body); + Member("generator", functionExpression.Generator); + Member("expression", ((IFunction) functionExpression).Expression); + // original Esprima doesn't include this information yet + if (_testCompatibilityMode != AstToJsonTestCompatibilityMode.EsprimaOrg) + { + Member("strict", functionExpression.Strict); + } + Member("async", functionExpression.Async); + } + + return functionExpression; + } + + protected internal override object? VisitIdentifier(Identifier identifier) + { + using (StartNodeObject(identifier)) + { + Member("name", identifier.Name); + } + + return identifier; + } + + protected internal override object? VisitIfStatement(IfStatement ifStatement) + { + using (StartNodeObject(ifStatement)) + { + Member("test", ifStatement.Test); + Member("consequent", ifStatement.Consequent); + Member("alternate", ifStatement.Alternate); + } + + return ifStatement; + } + + private object? VisitImportCompat(ImportCompat import) + { + EmptyNodeObject(import); + return import; + } + + private sealed class ImportCompat : Expression + { + public ImportCompat() : base(Nodes.Import) { } + + internal override Node? NextChildNode(ref ChildNodes.Enumerator enumerator) => null; + + protected internal override object? Accept(AstVisitor visitor) => ((AstToJsonConverter) visitor).VisitImportCompat(this); + } + + protected internal override object? VisitImport(Import import) + { + // original Esprima uses CallExpression to represent dynamic imports currently, + // so we need to rewrite our representation to match this expectation + if (_testCompatibilityMode == AstToJsonTestCompatibilityMode.EsprimaOrg) + { + const string importToken = "import"; + + var callee = new ImportCompat + { + Location = new Location(import.Location.Start, new Position(import.Location.Start.Line, import.Location.Start.Column + importToken.Length)), + Range = new Ast.Range(import.Range.Start, import.Range.Start + importToken.Length) + }; + var args = new NodeList(new Expression[] { import.Source }); + var callExpression = new CallExpression(callee, args, optional: false) + { + Location = import.Location, + Range = import.Range, + }; + + return Visit(callExpression); + } + + using (StartNodeObject(import)) + { + if (_testCompatibilityMode != AstToJsonTestCompatibilityMode.EsprimaOrg) + { + Member("source", import.Source); + + if (import.Attributes is not null) + { + Member("attributes", import.Attributes); + } + } + } + + return import; + } + + protected internal override object? VisitImportAttribute(ImportAttribute importAttribute) + { + using (StartNodeObject(importAttribute)) + { + Member("key", importAttribute.Key); + Member("value", importAttribute.Value); + } + + return importAttribute; + } + + protected internal override object? VisitImportDeclaration(ImportDeclaration importDeclaration) + { + using (StartNodeObject(importDeclaration)) + { + Member("specifiers", importDeclaration.Specifiers, e => (Node) e); + Member("source", importDeclaration.Source); + // original Esprima doesn't include this information yet + if (importDeclaration.Assertions.Count > 0) + { + Member("assertions", importDeclaration.Assertions); + } + } + + return importDeclaration; + } + + protected internal override object? VisitImportDefaultSpecifier(ImportDefaultSpecifier importDefaultSpecifier) + { + using (StartNodeObject(importDefaultSpecifier)) + { + Member("local", importDefaultSpecifier.Local); + } + + return importDefaultSpecifier; + } + + protected internal override object? VisitImportNamespaceSpecifier(ImportNamespaceSpecifier importNamespaceSpecifier) + { + using (StartNodeObject(importNamespaceSpecifier)) + { + Member("local", importNamespaceSpecifier.Local); + } + + return importNamespaceSpecifier; + } + + protected internal override object? VisitImportSpecifier(ImportSpecifier importSpecifier) + { + using (StartNodeObject(importSpecifier)) + { + Member("local", importSpecifier.Local); + Member("imported", importSpecifier.Imported); + } + + return importSpecifier; + } + + protected internal override object? VisitLabeledStatement(LabeledStatement labeledStatement) + { + using (StartNodeObject(labeledStatement)) + { + Member("label", labeledStatement.Label); + Member("body", labeledStatement.Body); + } + + return labeledStatement; + } + + protected internal override object? VisitLiteral(Literal literal) + { + using (StartNodeObject(literal)) + { + _writer.Member("value"); + var value = literal.Value; + + switch (value) + { + case null: + if (_testCompatibilityMode != AstToJsonTestCompatibilityMode.EsprimaOrg && literal.TokenType == TokenType.RegularExpression) + { + // This is how esprima.org actually renders regexes since it relies on Regex.toString + _writer.String(literal.Raw); + } + else + { + _writer.Null(); + } + + break; + case bool b: + _writer.Boolean(b); + break; + case Regex _: + _writer.StartObject(); + _writer.EndObject(); + break; + case double d: + _writer.Number(d); + break; + default: + _writer.String(System.Convert.ToString(value, CultureInfo.InvariantCulture)); + break; + } + + Member("raw", literal.Raw); + + if (literal.Regex is not null) + { + _writer.Member("regex"); + _writer.StartObject(); + Member("pattern", literal.Regex.Pattern); + Member("flags", literal.Regex.Flags); + _writer.EndObject(); + } + else if (literal.Value is BigInteger bigInt) + { + Member("bigint", bigInt.ToString(CultureInfo.InvariantCulture)); + } + } + + return literal; + } + + protected internal override object? VisitMemberExpression(MemberExpression memberExpression) + { + using (StartNodeObject(memberExpression)) + { + Member("computed", memberExpression.Computed); + Member("object", memberExpression.Object); + Member("property", memberExpression.Property); + Member("optional", memberExpression.Optional); + } + + return memberExpression; + } + + protected internal override object? VisitMetaProperty(MetaProperty metaProperty) + { + using (StartNodeObject(metaProperty)) + { + Member("meta", metaProperty.Meta); + Member("property", metaProperty.Property); + } + + return metaProperty; + } + + protected internal override object? VisitMethodDefinition(MethodDefinition methodDefinition) + { + using (StartNodeObject(methodDefinition)) + { + Member("key", methodDefinition.Key); + Member("computed", methodDefinition.Computed); + Member("value", methodDefinition.Value); + Member("kind", methodDefinition.Kind); + Member("static", methodDefinition.Static); + if (methodDefinition.Decorators.Count > 0) + { + Member("decorators", methodDefinition.Decorators); + } + } + + return methodDefinition; + } + + protected internal override object? VisitNewExpression(NewExpression newExpression) + { + using (StartNodeObject(newExpression)) + { + Member("callee", newExpression.Callee); + Member("arguments", newExpression.Arguments, e => (Node) e); + } + + return newExpression; + } + + protected internal override object? VisitObjectExpression(ObjectExpression objectExpression) + { + using (StartNodeObject(objectExpression)) + { + Member("properties", objectExpression.Properties); + } + + return objectExpression; + } + + protected internal override object? VisitObjectPattern(ObjectPattern objectPattern) + { + using (StartNodeObject(objectPattern)) + { + Member("properties", objectPattern.Properties); + } + + return objectPattern; + } + + protected internal override object? VisitPrivateIdentifier(PrivateIdentifier privateIdentifier) + { + using (StartNodeObject(privateIdentifier)) + { + Member("name", privateIdentifier.Name); + } + + return privateIdentifier; + } + + protected internal override object? VisitProgram(Program program) + { + using (StartNodeObject(program)) + { + Member("body", program.Body, e => (Node) e); + Member("sourceType", program.SourceType); + + // original Esprima doesn't include this information yet + if (_testCompatibilityMode != AstToJsonTestCompatibilityMode.EsprimaOrg && program is Script s) + { + Member("strict", s.Strict); + } + } + + return program; + } + + protected internal override object? VisitProperty(Property property) + { + using (StartNodeObject(property)) + { + Member("key", property.Key); + Member("computed", property.Computed); + Member("value", property.Value); + Member("kind", property.Kind); + Member("method", property.Method); + Member("shorthand", property.Shorthand); + } + + return property; + } + + protected internal override object? VisitPropertyDefinition(PropertyDefinition propertyDefinition) + { + using (StartNodeObject(propertyDefinition)) + { + Member("key", propertyDefinition.Key); + Member("computed", propertyDefinition.Computed); + Member("value", propertyDefinition.Value); + Member("kind", propertyDefinition.Kind); + Member("static", propertyDefinition.Static); + if (propertyDefinition.Decorators.Count > 0) + { + Member("decorators", propertyDefinition.Decorators); + } + } + + return propertyDefinition; + } + + protected internal override object? VisitRestElement(RestElement restElement) + { + using (StartNodeObject(restElement)) + { + Member("argument", restElement.Argument); + } + + return restElement; + } + + protected internal override object? VisitReturnStatement(ReturnStatement returnStatement) + { + using (StartNodeObject(returnStatement)) + { + Member("argument", returnStatement.Argument); + } + + return returnStatement; + } + + protected internal override object? VisitSequenceExpression(SequenceExpression sequenceExpression) + { + using (StartNodeObject(sequenceExpression)) + { + Member("expressions", sequenceExpression.Expressions); + } + + return sequenceExpression; + } + + protected internal override object? VisitSpreadElement(SpreadElement spreadElement) + { + using (StartNodeObject(spreadElement)) + { + Member("argument", spreadElement.Argument); + } + + return spreadElement; + } + + protected internal override object? VisitStaticBlock(StaticBlock staticBlock) + { + using (StartNodeObject(staticBlock)) + { + Member("body", staticBlock.Body, e => (Statement) e); + } + + return staticBlock; + } + + protected internal override object? VisitSuper(Super super) + { + EmptyNodeObject(super); + return super; + } + + protected internal override object? VisitSwitchCase(SwitchCase switchCase) + { + using (StartNodeObject(switchCase)) + { + Member("test", switchCase.Test); + Member("consequent", switchCase.Consequent, e => (Node) e); + } + + return switchCase; + } + + protected internal override object? VisitSwitchStatement(SwitchStatement switchStatement) + { + using (StartNodeObject(switchStatement)) + { + Member("discriminant", switchStatement.Discriminant); + Member("cases", switchStatement.Cases); + } + + return switchStatement; + } + + protected internal override object? VisitTaggedTemplateExpression(TaggedTemplateExpression taggedTemplateExpression) + { + using (StartNodeObject(taggedTemplateExpression)) + { + Member("tag", taggedTemplateExpression.Tag); + Member("quasi", taggedTemplateExpression.Quasi); + } + + return taggedTemplateExpression; + } + + protected internal override object? VisitTemplateElement(TemplateElement templateElement) + { + using (StartNodeObject(templateElement)) + { + _writer.Member("value"); + _writer.StartObject(); + Member("raw", templateElement.Value.Raw); + Member("cooked", templateElement.Value.Cooked); + _writer.EndObject(); + Member("tail", templateElement.Tail); + } + + return templateElement; + } + + protected internal override object? VisitTemplateLiteral(TemplateLiteral templateLiteral) + { + using (StartNodeObject(templateLiteral)) + { + Member("quasis", templateLiteral.Quasis); + Member("expressions", templateLiteral.Expressions); + } + + return templateLiteral; + } + + protected internal override object? VisitThisExpression(ThisExpression thisExpression) + { + EmptyNodeObject(thisExpression); + return thisExpression; + } + + protected internal override object? VisitThrowStatement(ThrowStatement throwStatement) + { + using (StartNodeObject(throwStatement)) + { + Member("argument", throwStatement.Argument); + } + + return throwStatement; + } + + protected internal override object? VisitTryStatement(TryStatement tryStatement) + { + using (StartNodeObject(tryStatement)) + { + Member("block", tryStatement.Block); + Member("handler", tryStatement.Handler); + Member("finalizer", tryStatement.Finalizer); + } + + return tryStatement; + } + + protected internal override object? VisitUnaryExpression(UnaryExpression unaryExpression) + { + using (StartNodeObject(unaryExpression)) + { + Member("operator", UnaryExpression.GetUnaryOperatorToken(unaryExpression.Operator)); + Member("argument", unaryExpression.Argument); + Member("prefix", unaryExpression.Prefix); + } + + return unaryExpression; + } + + protected internal override object? VisitVariableDeclaration(VariableDeclaration variableDeclaration) + { + using (StartNodeObject(variableDeclaration)) + { + Member("declarations", variableDeclaration.Declarations); + Member("kind", variableDeclaration.Kind); + } + + return variableDeclaration; + } + + protected internal override object? VisitVariableDeclarator(VariableDeclarator variableDeclarator) + { + using (StartNodeObject(variableDeclarator)) + { + Member("id", variableDeclarator.Id); + Member("init", variableDeclarator.Init); + } + + return variableDeclarator; + } + + protected internal override object? VisitWhileStatement(WhileStatement whileStatement) + { + using (StartNodeObject(whileStatement)) + { + Member("test", whileStatement.Test); + Member("body", whileStatement.Body); + } + + return whileStatement; + } + + protected internal override object? VisitWithStatement(WithStatement withStatement) + { + using (StartNodeObject(withStatement)) + { + Member("object", withStatement.Object); + Member("body", withStatement.Body); + } + + return withStatement; + } + + protected internal override object? VisitYieldExpression(YieldExpression yieldExpression) + { + using (StartNodeObject(yieldExpression)) + { + Member("argument", yieldExpression.Argument); + Member("delegate", yieldExpression.Delegate); + } + + return yieldExpression; + } +} diff --git a/src/Esprima/Utils/EnumHelper.cs b/src/Esprima/Utils/EnumHelper.cs new file mode 100644 index 00000000..72f6fbfd --- /dev/null +++ b/src/Esprima/Utils/EnumHelper.cs @@ -0,0 +1,36 @@ +using System.Runtime.CompilerServices; + +namespace Esprima.Utils; + +internal static class EnumHelper +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TEnum ToFlag(this bool value, TEnum flag) where TEnum : struct, Enum => + value.ToFlag(flag, default); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TEnum ToFlag(this bool value, TEnum flag, TEnum fallbackFlag) where TEnum : struct, Enum => + value ? flag : fallbackFlag; + + // Enum.HasFlag is slow (at least, on older runtimes). However, a non-allocating, generic solution would require System.Runtime.CompilerServices.Unsafe: + // https://github.com/dotnet/csharplang/discussions/1993#discussioncomment-104840 + // In case System.Runtime.CompilerServices.Unsafe becomes available, these methods should be replaced with a generic implementation. + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasFlagFast(this AstToJavascriptConverter.BinaryOperationFlags flags, AstToJavascriptConverter.BinaryOperationFlags flag) => (flags & flag) == flag; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasFlagFast(this AstToJavascriptConverter.StatementFlags flags, AstToJavascriptConverter.StatementFlags flag) => (flags & flag) == flag; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasFlagFast(this AstToJavascriptConverter.ExpressionFlags flags, AstToJavascriptConverter.ExpressionFlags flag) => (flags & flag) == flag; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasFlagFast(this JavascriptTextWriter.TokenFlags flags, JavascriptTextWriter.TokenFlags flag) => (flags & flag) == flag; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasFlagFast(this JavascriptTextWriter.StatementFlags flags, JavascriptTextWriter.StatementFlags flag) => (flags & flag) == flag; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasFlagFast(this JavascriptTextWriter.ExpressionFlags flags, JavascriptTextWriter.ExpressionFlags flag) => (flags & flag) == flag; +} diff --git a/src/Esprima/Utils/ExpressionHelper.cs b/src/Esprima/Utils/ExpressionHelper.cs new file mode 100644 index 00000000..350542f3 --- /dev/null +++ b/src/Esprima/Utils/ExpressionHelper.cs @@ -0,0 +1,153 @@ +using Esprima.Ast; + +namespace Esprima.Utils; + +internal static class ExpressionHelper +{ + /// + /// Maps operator precedence to an integer value. + /// + /// The expression representing the operation. + /// + /// If less than zero, the operation has left-to-right associativity.
+ /// If zero, associativity is not defined for the operation.
+ /// If greater than zero, the operation has right-to-left associativity. + /// + /// + /// Precedence value as defined based on this table. Higher value means higher precedence. + /// Negative value is returned if the precedence is not defined for the specified expression. is returned for primitive expressions like . + /// + public static int GetOperatorPrecedence(this Expression expression, out int associativity) + { + const int leftToRightAssociativity = -1; + const int undefinedAssociativity = 0; + const int rightToLeftAssociativity = 1; + + associativity = undefinedAssociativity; + +Reenter: + switch (expression.Type) + { + case Nodes.ArrayExpression: + case Nodes.ClassExpression: + case Nodes.FunctionExpression: + case Nodes.Identifier: + case Nodes.Literal: + case Nodes.ObjectExpression: + case Nodes.PrivateIdentifier: + case Nodes.Super: + case Nodes.TaggedTemplateExpression: + case Nodes.TemplateLiteral: + case Nodes.ThisExpression: + return int.MaxValue; + + case Nodes.MemberExpression when !expression.As().Computed: + case Nodes.MetaProperty: + associativity = leftToRightAssociativity; + goto case Nodes.MemberExpression; + case Nodes.MemberExpression: + case Nodes.CallExpression: + case Nodes.Import: + case Nodes.NewExpression when expression.As().Arguments.Count > 0: + return 1700; + + case Nodes.NewExpression: + return 1600; + + case Nodes.UpdateExpression when !expression.As().Prefix: + return 1500; + + case Nodes.UpdateExpression: + case Nodes.UnaryExpression: + case Nodes.AwaitExpression: + return 1400; + + case Nodes.BinaryExpression: + switch (expression.As().Operator) + { + case BinaryOperator.Exponentiation: + associativity = rightToLeftAssociativity; + return 1300; + + case BinaryOperator.Times: + case BinaryOperator.Divide: + case BinaryOperator.Modulo: + associativity = leftToRightAssociativity; + return 1200; + + case BinaryOperator.Plus: + case BinaryOperator.Minus: + associativity = leftToRightAssociativity; + return 1100; + + case BinaryOperator.LeftShift: + case BinaryOperator.RightShift: + case BinaryOperator.UnsignedRightShift: + associativity = leftToRightAssociativity; + return 1000; + + case BinaryOperator.Less: + case BinaryOperator.LessOrEqual: + case BinaryOperator.Greater: + case BinaryOperator.GreaterOrEqual: + case BinaryOperator.In: + case BinaryOperator.InstanceOf: + associativity = leftToRightAssociativity; + return 900; + + case BinaryOperator.Equal: + case BinaryOperator.NotEqual: + case BinaryOperator.StrictlyEqual: + case BinaryOperator.StricltyNotEqual: + associativity = leftToRightAssociativity; + return 800; + + case BinaryOperator.BitwiseAnd: + associativity = leftToRightAssociativity; + return 700; + + case BinaryOperator.BitwiseXor: + associativity = leftToRightAssociativity; + return 600; + + case BinaryOperator.BitwiseOr: + associativity = leftToRightAssociativity; + return 500; + } + break; + + case Nodes.LogicalExpression: + switch (expression.As().Operator) + { + case BinaryOperator.LogicalAnd: + associativity = leftToRightAssociativity; + return 400; + case BinaryOperator.LogicalOr: + case BinaryOperator.NullishCoalescing: + associativity = leftToRightAssociativity; + return 300; + } + break; + + case Nodes.AssignmentExpression: + case Nodes.ConditionalExpression: + associativity = rightToLeftAssociativity; + goto case Nodes.ArrowFunctionExpression; + case Nodes.ArrowFunctionExpression: + case Nodes.YieldExpression: + case Nodes.SpreadElement: + return 200; + + case Nodes.SequenceExpression: + associativity = leftToRightAssociativity; + return 100; + + case Nodes.ChainExpression: + // This can be improved when tail recursion becomes available (see https://github.com/dotnet/csharplang/issues/2304). + expression = expression.As().Expression; + goto Reenter; + } + + return -1; + } +} diff --git a/src/Esprima/Utils/JavascriptTextWriter.Enums.cs b/src/Esprima/Utils/JavascriptTextWriter.Enums.cs new file mode 100644 index 00000000..52a8f297 --- /dev/null +++ b/src/Esprima/Utils/JavascriptTextWriter.Enums.cs @@ -0,0 +1,119 @@ +using Esprima.Ast; + +namespace Esprima.Utils; + +partial class JavascriptTextWriter +{ + [Flags] + public enum TokenFlags + { + None = 0, + + // Position hints for punctuators (exclusive, i.e at most one of these flags should be set) + + /// + /// The punctuator precedes the related token(s). + /// + Leading = 1 << 0, + /// + /// The punctuator is somewhere in the middle of the related token(s). + /// + InBetween = 1 << 1, + /// + /// The punctuator follows the related token(s). + /// + Trailing = 1 << 2, + + // Whitespace hints for keywords + + /// + /// The keyword follows the body of a statement and precedes another body of the same statement (e.g. the else branch of an ). + /// + FollowsStatementBody = StatementFlags.IsStatementBody, + + // General whitespace hints + + /// + /// A leading space is recommended for the current token (unless other white-space precedes it). + /// + /// + /// May or may not be respected. (It is decided by the actual implementation.) + /// + LeadingSpaceRecommended = 1 << 14, + /// + /// A trailing space is recommended for the current token (unless other white-space follows it). + /// + /// + /// May or may not be respected. (It is decided by the actual implementation.) + /// + TrailingSpaceRecommended = 1 << 15, + + /// + /// Surrounding spaces are recommended for the current token (unless other white-spaces surround it). + /// + /// + /// May or may not be respected. (It is decided by the actual implementation.) + /// + SurroundingSpaceRecommended = LeadingSpaceRecommended | TrailingSpaceRecommended, + } + + [Flags] + public enum StatementFlags + { + // Notes for maintainers: + // Don't use the high-order word as it's reserved for internal use (see AstToJavascriptConverter.StatementFlags) + + None = 0, + /// + /// The statement must be terminated with a semicolon. + /// + NeedsSemicolon = 1 << 0, + /// + /// If is set, determines if the semicolon can be omitted when the statement comes last in the current block (see ). + /// + /// + /// Automatically propagated to child statements, should be set directly only for statement list items. + /// Whether the semicolon is omitted or not is decided by the actual implementation. + /// + MayOmitRightMostSemicolon = 1 << 1, + /// + /// The statement comes last in the current statement list (more precisely, it is the right-most part in the textual representation of the current statement list). + /// + /// + /// In the the visitation handlers of the flag is interpreted differently: it indicates that the statement comes last in the parent statement. + /// (Upon visiting a statement, this flag of the parent and child statement gets combined to determine its effective value for the current statement list.) + /// + IsRightMost = 1 << 2, + /// + /// The statement represents the body of another statement (e.g. the if branch of an ). + /// + IsStatementBody = 1 << 3, + } + + [Flags] + public enum ExpressionFlags + { + // Notes for maintainers: + // Don't use the high-order word as it's reserved for internal use (see AstToJavascriptConverter.ExpressionFlags) + + None = 0, + /// + /// The expression must be wrapped in brackets. + /// + NeedsBrackets = 1 << 0, + /// + /// The expression comes first in the current expression tree, more precisely, it is the left-most part in the textual representation of the currently visited expression tree (incl. brackets). + /// + /// + /// In the the visitation handlers of the flag is interpreted differently: it indicates that the expression comes first in the parent expression. + /// (Upon visiting an expression, this flag of the parent and child expression gets combined to determine its effective value for the expression tree.) + /// + IsLeftMost = 1 << 1, + + // White-space hints + + SpaceBeforeBracketsRecommended = 1 << 14, + SpaceAfterBracketsRecommended = 1 << 15, + SpaceAroundBracketsRecommended = SpaceBeforeBracketsRecommended | SpaceAfterBracketsRecommended, + } +} diff --git a/src/Esprima/Utils/JavascriptTextWriter.WriteContext.cs b/src/Esprima/Utils/JavascriptTextWriter.WriteContext.cs new file mode 100644 index 00000000..5ca5ca93 --- /dev/null +++ b/src/Esprima/Utils/JavascriptTextWriter.WriteContext.cs @@ -0,0 +1,92 @@ +using System.Runtime.CompilerServices; +using Esprima.Ast; +using static Esprima.EsprimaExceptionHelper; + +namespace Esprima.Utils; + +partial class JavascriptTextWriter +{ + public struct WriteContext + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public WriteContext From(Node? parentNode, Node node) => + new WriteContext(parentNode, node ?? ThrowArgumentNullException(nameof(node))); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal WriteContext(Node? parentNode, Node node) + { + ParentNode = parentNode; + Node = node; + _nodePropertyName = null; + _nodePropertyValueAccessor = null; + Data = null; + } + + public Node? ParentNode { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } + public Node Node { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } + + private string? _nodePropertyName; + public string? NodePropertyName { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _nodePropertyName; } + + private Delegate? _nodePropertyValueAccessor; + private Delegate NodePropertyAccessor + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _nodePropertyValueAccessor ?? ThrowInvalidOperationException("The context has no associated node property."); + } + + public bool NodePropertyHasListValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => NodePropertyAccessor.GetType().IsGenericType; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Type GetNodePropertyListItemType() + { + var type = NodePropertyAccessor.GetType(); + return type.IsGenericType + ? type.GetGenericArguments()[0] + : ThrowInvalidOperationException("The context has an associated node property but its value is not a node list."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T GetNodePropertyValue() => + (T) ((NodePropertyValueAccessor) NodePropertyAccessor)(Node)!; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref readonly NodeList GetNodePropertyListValue() where T : Node? => + ref ((NodePropertyListValueAccessor) NodePropertyAccessor)(Node); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearNodeProperty() + { + _nodePropertyName = null; + _nodePropertyValueAccessor = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void SetNodeProperty(string name, NodePropertyValueAccessor valueAccessor) + { + _nodePropertyName = name; + _nodePropertyValueAccessor = valueAccessor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ChangeNodeProperty(string name, NodePropertyValueAccessor valueAccessor) => + SetNodeProperty(name ?? ThrowArgumentNullException(nameof(name)), valueAccessor ?? ThrowArgumentNullException(nameof(valueAccessor))); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void SetNodeProperty(string name, NodePropertyListValueAccessor listValueAccessor) where T : Node? + { + _nodePropertyName = name; + _nodePropertyValueAccessor = listValueAccessor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ChangeNodeProperty(string name, NodePropertyListValueAccessor listValueAccessor) where T : Node? => + SetNodeProperty(name ?? ThrowArgumentNullException(nameof(name)), listValueAccessor ?? ThrowArgumentNullException>(nameof(listValueAccessor))); + + public object? Data; + } +} diff --git a/src/Esprima/Utils/JavascriptTextWriter.cs b/src/Esprima/Utils/JavascriptTextWriter.cs new file mode 100644 index 00000000..57eeb3e8 --- /dev/null +++ b/src/Esprima/Utils/JavascriptTextWriter.cs @@ -0,0 +1,331 @@ +using System.Runtime.CompilerServices; +using Esprima.Ast; + +namespace Esprima.Utils; + +public delegate object? NodePropertyValueAccessor(Node node); + +public delegate ref readonly NodeList NodePropertyListValueAccessor(Node node) where T : Node?; + +public record class JavascriptTextWriterOptions +{ + public static readonly JavascriptTextWriterOptions Default = new(); + + protected internal virtual JavascriptTextWriter CreateWriter(TextWriter writer) => new JavascriptTextWriter(writer, this); +} + +/// +/// Base Javascript text writer (code formatter) which uses the most compact possible (i.e. minimal) format. +/// +public partial class JavascriptTextWriter +{ + private readonly TextWriter _writer; + + public JavascriptTextWriter(TextWriter writer, JavascriptTextWriterOptions options) + { + _writer = writer ?? throw new ArgumentNullException(nameof(writer)); + + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + LastTokenType = TokenType.EOF; + WhiteSpaceWrittenSinceLastToken = true; + } + + protected TokenType LastTokenType { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] private set; } + protected TokenFlags LastTokenFlags { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] private set; } + protected bool WhiteSpaceWrittenSinceLastToken { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void WriteSpace() + { + WriteWhiteSpace(" "); + } + + protected void WriteLine() + { + _writer.WriteLine(); + WhiteSpaceWrittenSinceLastToken = true; + } + + protected void WriteWhiteSpace(string value) + { + _writer.Write(value); + WhiteSpaceWrittenSinceLastToken = true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void ForceRecommendedSpace() + { + LastTokenFlags |= TokenFlags.TrailingSpaceRecommended; + } + + public virtual void WriteEpsilon(TokenFlags flags, ref WriteContext context) { } + + protected virtual void StartIdentifier(string value, TokenFlags flags, ref WriteContext context) + { + switch (LastTokenType) + { + case TokenType.BigIntLiteral: + case TokenType.BooleanLiteral: + case TokenType.Identifier: + case TokenType.Keyword: + case TokenType.NullLiteral: + case TokenType.NumericLiteral: + case TokenType.RegularExpression: + WriteSpace(); + break; + case TokenType.EOF: + case TokenType.Punctuator: + case TokenType.StringLiteral: + case TokenType.Template: + break; + default: + throw new InvalidOperationException(); + } + } + + public void WriteIdentifier(string value, TokenFlags flags, ref WriteContext context) + { + StartIdentifier(value, flags, ref context); + _writer.Write(value); + WhiteSpaceWrittenSinceLastToken = false; + EndIdentifier(value, flags, ref context); + + LastTokenType = TokenType.Identifier; + LastTokenFlags = flags; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteIdentifier(string value, ref WriteContext context) + { + WriteIdentifier(value, TokenFlags.None, ref context); + } + + protected virtual void EndIdentifier(string value, TokenFlags flags, ref WriteContext context) { } + + protected virtual void StartKeyword(string value, TokenFlags flags, ref WriteContext context) + { + switch (LastTokenType) + { + case TokenType.BigIntLiteral: + case TokenType.BooleanLiteral: + case TokenType.Identifier: + case TokenType.Keyword: + case TokenType.NullLiteral: + case TokenType.NumericLiteral: + case TokenType.RegularExpression: + WriteSpace(); + break; + case TokenType.EOF: + case TokenType.Punctuator: + case TokenType.StringLiteral: + case TokenType.Template: + break; + default: + throw new InvalidOperationException(); + } + } + + public void WriteKeyword(string value, TokenFlags flags, ref WriteContext context) + { + StartKeyword(value, flags, ref context); + _writer.Write(value); + WhiteSpaceWrittenSinceLastToken = false; + EndKeyword(value, flags, ref context); + + LastTokenType = TokenType.Keyword; + LastTokenFlags = flags; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteKeyword(string value, ref WriteContext context) + { + WriteKeyword(value, TokenFlags.None, ref context); + } + + protected virtual void EndKeyword(string value, TokenFlags flags, ref WriteContext context) { } + + protected virtual void StartLiteral(string value, TokenType type, TokenFlags flags, ref WriteContext context) + { + switch (LastTokenType) + { + case TokenType.BigIntLiteral: + case TokenType.BooleanLiteral: + case TokenType.Identifier: + case TokenType.Keyword: + case TokenType.NullLiteral: + case TokenType.NumericLiteral: + case TokenType.RegularExpression: + if (type is not (TokenType.StringLiteral or TokenType.RegularExpression)) + { + WriteSpace(); + } + break; + case TokenType.EOF: + case TokenType.Punctuator: + case TokenType.StringLiteral: + case TokenType.Template: + break; + default: + throw new InvalidOperationException(); + } + } + + public void WriteLiteral(string value, TokenType type, TokenFlags flags, ref WriteContext context) + { + StartLiteral(value, type, flags, ref context); + _writer.Write(value); + WhiteSpaceWrittenSinceLastToken = false; + EndLiteral(value, type, flags, ref context); + + LastTokenType = type; + LastTokenFlags = flags; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteLiteral(string value, TokenType tokenType, ref WriteContext context) + { + WriteLiteral(value, tokenType, TokenFlags.None, ref context); + } + + protected virtual void EndLiteral(string value, TokenType type, TokenFlags flags, ref WriteContext context) { } + + protected virtual void StartPunctuator(string value, TokenFlags flags, ref WriteContext context) { } + + public void WritePunctuator(string value, TokenFlags flags, ref WriteContext context) + { + StartPunctuator(value, flags, ref context); + _writer.Write(value); + WhiteSpaceWrittenSinceLastToken = false; + EndPunctuator(value, flags, ref context); + + LastTokenType = TokenType.Punctuator; + LastTokenFlags = flags; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePunctuator(string value, ref WriteContext context) + { + WritePunctuator(value, TokenFlags.None, ref context); + } + + protected virtual void EndPunctuator(string value, TokenFlags flags, ref WriteContext context) { } + + public virtual void StartArray(int elementCount, ref WriteContext context) + { + WritePunctuator("[", TokenFlags.Leading, ref context); + } + + public virtual void EndArray(int elementCount, ref WriteContext context) + { + WritePunctuator("]", TokenFlags.Trailing, ref context); + } + + public virtual void StartObject(int propertyCount, ref WriteContext context) + { + WritePunctuator("{", TokenFlags.Leading | TokenFlags.TrailingSpaceRecommended, ref context); + } + + public virtual void EndObject(int propertyCount, ref WriteContext context) + { + WritePunctuator("}", TokenFlags.Trailing | TokenFlags.LeadingSpaceRecommended, ref context); + } + + public virtual void StartBlock(int statementCount, ref WriteContext context) + { + WritePunctuator("{", TokenFlags.Leading | TokenFlags.SurroundingSpaceRecommended, ref context); + } + + public virtual void EndBlock(int statementCount, ref WriteContext context) + { + WritePunctuator("}", TokenFlags.Trailing | TokenFlags.LeadingSpaceRecommended, ref context); + } + + public virtual void StartStatement(StatementFlags flags, ref WriteContext context) { } + + public virtual void EndStatement(StatementFlags flags, ref WriteContext context) + { + // Writes statement terminator unless it can be omitted. + if (flags.HasFlagFast(StatementFlags.NeedsSemicolon) && !flags.HasFlagFast(StatementFlags.MayOmitRightMostSemicolon | StatementFlags.IsRightMost)) + { + WritePunctuator(";", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref context); + } + } + + public virtual void StartStatementList(int count, ref WriteContext context) { } + + public virtual void StartStatementListItem(int index, int count, StatementFlags flags, ref WriteContext context) { } + + public virtual void EndStatementListItem(int index, int count, StatementFlags flags, ref WriteContext context) + { + // Writes statement terminator unless it can be omitted. + if (flags.HasFlagFast(StatementFlags.NeedsSemicolon) && !flags.HasFlagFast(StatementFlags.MayOmitRightMostSemicolon | StatementFlags.IsRightMost)) + { + WritePunctuator(";", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref context); + } + } + + public virtual void EndStatementList(int count, ref WriteContext context) { } + + public virtual void StartExpression(ExpressionFlags flags, ref WriteContext context) + { + if (flags.HasFlagFast(ExpressionFlags.NeedsBrackets)) + { + WritePunctuator("(", TokenFlags.Leading | flags.HasFlagFast(ExpressionFlags.SpaceAroundBracketsRecommended).ToFlag(TokenFlags.LeadingSpaceRecommended), ref context); + } + } + + public virtual void EndExpression(ExpressionFlags flags, ref WriteContext context) + { + if (flags.HasFlagFast(ExpressionFlags.NeedsBrackets)) + { + WritePunctuator(")", TokenFlags.Trailing | flags.HasFlagFast(ExpressionFlags.SpaceAroundBracketsRecommended).ToFlag(TokenFlags.TrailingSpaceRecommended), ref context); + } + } + + public virtual void StartExpressionList(int count, ref WriteContext context) { } + + public virtual void StartExpressionListItem(int index, int count, ExpressionFlags flags, ref WriteContext context) + { + if (flags.HasFlagFast(ExpressionFlags.NeedsBrackets)) + { + WritePunctuator("(", TokenFlags.Leading, ref context); + } + } + + public virtual void EndExpressionListItem(int index, int count, ExpressionFlags flags, ref WriteContext context) + { + if (flags.HasFlagFast(ExpressionFlags.NeedsBrackets)) + { + WritePunctuator(")", TokenFlags.Trailing, ref context); + } + + if (index < count - 1) + { + WritePunctuator(",", TokenFlags.InBetween | TokenFlags.TrailingSpaceRecommended, ref context); + } + } + + public virtual void EndExpressionList(int count, ref WriteContext context) { } + + public virtual void StartAuxiliaryNode(object? nodeContext, ref WriteContext context) { } + + public virtual void EndAuxiliaryNode(object? nodeContext, ref WriteContext context) { } + + public virtual void StartAuxiliaryNodeList(int count, ref WriteContext context) where T : Node? { } + + public virtual void StartAuxiliaryNodeListItem(int index, int count, string separator, object? nodeContext, ref WriteContext context) where T : Node? { } + + public virtual void EndAuxiliaryNodeListItem(int index, int count, string separator, object? nodeContext, ref WriteContext context) where T : Node? + { + if (separator.Length > 0 && index < count - 1) + { + WritePunctuator(separator, TokenFlags.InBetween | TokenFlags.TrailingSpaceRecommended, ref context); + } + } + + public virtual void EndAuxiliaryNodeList(int count, ref WriteContext context) where T : Node? { } +} diff --git a/src/Esprima/Utils/JsonTextWriter.cs b/src/Esprima/Utils/JsonTextWriter.cs index 8eb90017..95fb2e3e 100644 --- a/src/Esprima/Utils/JsonTextWriter.cs +++ b/src/Esprima/Utils/JsonTextWriter.cs @@ -53,8 +53,13 @@ public JsonTextWriter(TextWriter writer) : public JsonTextWriter(TextWriter writer, string? indent) { _writer = writer ?? ThrowArgumentNullException(nameof(writer)); - _writer = writer ?? ThrowArgumentNullException(nameof(writer)); + + if (!string.IsNullOrWhiteSpace(indent)) + { + throw new ArgumentException("Indent must be null or white-space.", nameof(indent)); + } _indent = indent ?? ""; + _counters = new Stack(8); _structures = new Stack(8); } diff --git a/src/Esprima/Utils/Jsx/JsxAstJson.cs b/src/Esprima/Utils/Jsx/JsxAstJson.cs deleted file mode 100644 index 52ed69f6..00000000 --- a/src/Esprima/Utils/Jsx/JsxAstJson.cs +++ /dev/null @@ -1,172 +0,0 @@ -using Esprima.Ast; -using Esprima.Ast.Jsx; - -namespace Esprima.Utils.Jsx; - -public sealed class JsxAstToJsonConverter : AstToJsonConverter -{ - public static new readonly JsxAstToJsonConverter Default = new(); - - private JsxAstToJsonConverter() { } - - private protected override VisitorBase CreateVisitor(JsonWriter writer, AstJson.Options options) - { - return new Visitor(writer, options); - } - - private sealed class Visitor : VisitorBase, IJsxAstVisitor - { - public Visitor(JsonWriter writer, AstJson.Options options) - : base(writer, options) - { - } - - protected override string GetNodeType(Node node) - { - if (node is JsxExpression jsxExpression) - { - // Due to the borrowed test fixtures, it's important to use the 'JSX' prefix to stay consistent with the naming used by original Esprima - // (see https://github.com/jquery/esprima/blob/4.0.1/src/jsx-nodes.ts). - return "JSX" + jsxExpression.Type.ToString(); - } - - return base.GetNodeType(node); - } - - object? IJsxAstVisitor.VisitJsxAttribute(JsxAttribute jsxAttribute) - { - using (StartNodeObject(jsxAttribute)) - { - Member("name", jsxAttribute.Name); - Member("value", jsxAttribute.Value); - } - - return jsxAttribute; - } - - object? IJsxAstVisitor.VisitJsxClosingElement(JsxClosingElement jsxClosingElement) - { - using (StartNodeObject(jsxClosingElement)) - { - Member("name", jsxClosingElement.Name); - } - - return jsxClosingElement; - } - - object? IJsxAstVisitor.VisitJsxClosingFragment(JsxClosingFragment jsxClosingFragment) - { - using (StartNodeObject(jsxClosingFragment)) - { - } - - return jsxClosingFragment; - } - - object? IJsxAstVisitor.VisitJsxElement(JsxElement jsxElement) - { - using (StartNodeObject(jsxElement)) - { - Member("openingElement", jsxElement.OpeningElement); - Member("children", jsxElement.Children); - Member("closingElement", jsxElement.ClosingElement); - } - - return jsxElement; - } - - object? IJsxAstVisitor.VisitJsxEmptyExpression(JsxEmptyExpression jsxEmptyExpression) - { - using (StartNodeObject(jsxEmptyExpression)) - { - } - - return jsxEmptyExpression; - } - - object? IJsxAstVisitor.VisitJsxExpressionContainer(JsxExpressionContainer jsxExpressionContainer) - { - using (StartNodeObject(jsxExpressionContainer)) - { - Member("expression", jsxExpressionContainer.Expression); - } - - return jsxExpressionContainer; - } - - object? IJsxAstVisitor.VisitJsxIdentifier(JsxIdentifier jsxIdentifier) - { - using (StartNodeObject(jsxIdentifier)) - { - Member("name", jsxIdentifier.Name); - } - - return jsxIdentifier; - } - - object? IJsxAstVisitor.VisitJsxMemberExpression(JsxMemberExpression jsxMemberExpression) - { - using (StartNodeObject(jsxMemberExpression)) - { - Member("object", jsxMemberExpression.Object); - Member("property", jsxMemberExpression.Property); - } - - return jsxMemberExpression; - } - - object? IJsxAstVisitor.VisitJsxNamespacedName(JsxNamespacedName jsxNamespacedName) - { - using (StartNodeObject(jsxNamespacedName)) - { - Member("namespace", jsxNamespacedName.Namespace); - Member("name", jsxNamespacedName.Name); - } - - return jsxNamespacedName; - } - - object? IJsxAstVisitor.VisitJsxOpeningElement(JsxOpeningElement jsxOpeningElement) - { - using (StartNodeObject(jsxOpeningElement)) - { - Member("name", jsxOpeningElement.Name); - Member("selfClosing", jsxOpeningElement.SelfClosing); - Member("attributes", jsxOpeningElement.Attributes); - } - - return jsxOpeningElement; - } - - object? IJsxAstVisitor.VisitJsxOpeningFragment(JsxOpeningFragment jsxOpeningFragment) - { - using (StartNodeObject(jsxOpeningFragment)) - { - Member("selfClosing", jsxOpeningFragment.SelfClosing); - } - - return jsxOpeningFragment; - } - - object? IJsxAstVisitor.VisitJsxSpreadAttribute(JsxSpreadAttribute jsxSpreadAttribute) - { - using (StartNodeObject(jsxSpreadAttribute)) - { - Member("argument", jsxSpreadAttribute.Argument); - } - - return jsxSpreadAttribute; - } - - object? IJsxAstVisitor.VisitJsxText(JsxText jsxText) - { - using (StartNodeObject(jsxText)) - { - Member("value", jsxText.Value); - Member("raw", jsxText.Raw); - } - - return jsxText; - } - } -} diff --git a/src/Esprima/Utils/Jsx/JsxAstToJsonConverter.cs b/src/Esprima/Utils/Jsx/JsxAstToJsonConverter.cs new file mode 100644 index 00000000..6d385820 --- /dev/null +++ b/src/Esprima/Utils/Jsx/JsxAstToJsonConverter.cs @@ -0,0 +1,167 @@ +using Esprima.Ast; +using Esprima.Ast.Jsx; + +namespace Esprima.Utils.Jsx; + +public record class JsxAstToJsonOptions : AstToJsonOptions +{ + public static new readonly JsxAstToJsonOptions Default = new(); + + protected internal override AstToJsonConverter CreateConverter(JsonWriter writer) => new JsxAstToJsonConverter(writer, this); +} + +public class JsxAstToJsonConverter : AstToJsonConverter, IJsxAstVisitor +{ + public JsxAstToJsonConverter(JsonWriter writer, JsxAstToJsonOptions options) + : base(writer, options) + { + } + + protected override string GetNodeType(Node node) + { + if (node is JsxExpression jsxExpression) + { + // Due to the borrowed test fixtures, it's important to use the 'JSX' prefix to stay consistent with the naming used by original Esprima + // (see https://github.com/jquery/esprima/blob/4.0.1/src/jsx-nodes.ts). + return "JSX" + jsxExpression.Type.ToString(); + } + + return base.GetNodeType(node); + } + + object? IJsxAstVisitor.VisitJsxAttribute(JsxAttribute jsxAttribute) + { + using (StartNodeObject(jsxAttribute)) + { + Member("name", jsxAttribute.Name); + Member("value", jsxAttribute.Value); + } + + return jsxAttribute; + } + + object? IJsxAstVisitor.VisitJsxClosingElement(JsxClosingElement jsxClosingElement) + { + using (StartNodeObject(jsxClosingElement)) + { + Member("name", jsxClosingElement.Name); + } + + return jsxClosingElement; + } + + object? IJsxAstVisitor.VisitJsxClosingFragment(JsxClosingFragment jsxClosingFragment) + { + using (StartNodeObject(jsxClosingFragment)) + { + } + + return jsxClosingFragment; + } + + object? IJsxAstVisitor.VisitJsxElement(JsxElement jsxElement) + { + using (StartNodeObject(jsxElement)) + { + Member("openingElement", jsxElement.OpeningElement); + Member("children", jsxElement.Children); + Member("closingElement", jsxElement.ClosingElement); + } + + return jsxElement; + } + + object? IJsxAstVisitor.VisitJsxEmptyExpression(JsxEmptyExpression jsxEmptyExpression) + { + using (StartNodeObject(jsxEmptyExpression)) + { + } + + return jsxEmptyExpression; + } + + object? IJsxAstVisitor.VisitJsxExpressionContainer(JsxExpressionContainer jsxExpressionContainer) + { + using (StartNodeObject(jsxExpressionContainer)) + { + Member("expression", jsxExpressionContainer.Expression); + } + + return jsxExpressionContainer; + } + + object? IJsxAstVisitor.VisitJsxIdentifier(JsxIdentifier jsxIdentifier) + { + using (StartNodeObject(jsxIdentifier)) + { + Member("name", jsxIdentifier.Name); + } + + return jsxIdentifier; + } + + object? IJsxAstVisitor.VisitJsxMemberExpression(JsxMemberExpression jsxMemberExpression) + { + using (StartNodeObject(jsxMemberExpression)) + { + Member("object", jsxMemberExpression.Object); + Member("property", jsxMemberExpression.Property); + } + + return jsxMemberExpression; + } + + object? IJsxAstVisitor.VisitJsxNamespacedName(JsxNamespacedName jsxNamespacedName) + { + using (StartNodeObject(jsxNamespacedName)) + { + Member("namespace", jsxNamespacedName.Namespace); + Member("name", jsxNamespacedName.Name); + } + + return jsxNamespacedName; + } + + object? IJsxAstVisitor.VisitJsxOpeningElement(JsxOpeningElement jsxOpeningElement) + { + using (StartNodeObject(jsxOpeningElement)) + { + Member("name", jsxOpeningElement.Name); + Member("selfClosing", jsxOpeningElement.SelfClosing); + Member("attributes", jsxOpeningElement.Attributes); + } + + return jsxOpeningElement; + } + + object? IJsxAstVisitor.VisitJsxOpeningFragment(JsxOpeningFragment jsxOpeningFragment) + { + using (StartNodeObject(jsxOpeningFragment)) + { + Member("selfClosing", jsxOpeningFragment.SelfClosing); + } + + return jsxOpeningFragment; + } + + object? IJsxAstVisitor.VisitJsxSpreadAttribute(JsxSpreadAttribute jsxSpreadAttribute) + { + using (StartNodeObject(jsxSpreadAttribute)) + { + Member("argument", jsxSpreadAttribute.Argument); + } + + return jsxSpreadAttribute; + } + + object? IJsxAstVisitor.VisitJsxText(JsxText jsxText) + { + using (StartNodeObject(jsxText)) + { + Member("value", jsxText.Value); + Member("raw", jsxText.Raw); + } + + return jsxText; + } +} diff --git a/src/Esprima/Utils/KnRJavascriptTextWriter.cs b/src/Esprima/Utils/KnRJavascriptTextWriter.cs new file mode 100644 index 00000000..43e27ceb --- /dev/null +++ b/src/Esprima/Utils/KnRJavascriptTextWriter.cs @@ -0,0 +1,457 @@ +using System.Runtime.CompilerServices; +using Esprima.Ast; + +namespace Esprima.Utils; + +public record class KnRJavascriptTextWriterOptions : JavascriptTextWriterOptions +{ + public static new readonly KnRJavascriptTextWriterOptions Default = new(); + + public string? Indent { get; init; } + public bool UseEgyptianBraces { get; init; } = true; + public bool KeepSingleStatementBodyInLine { get; init; } + public bool KeepEmptyBlockBodyInLine { get; init; } = true; + public int MultiLineArrayLiteralThreshold { get; init; } = 7; + public int MultiLineObjectLiteralThreshold { get; init; } = 3; + + protected internal override JavascriptTextWriter CreateWriter(TextWriter writer) => new KnRJavascriptTextWriter(writer, this); +} + +/// +/// Javascript text writer (code formatter) which implements the most common K&R style. +/// +public class KnRJavascriptTextWriter : JavascriptTextWriter +{ + private const int UseEgyptianBracesFlag = 1 << 0; + private const int KeepSingleStatementBodyInLineFlag = 1 << 1; + private const int KeepEmptyBlockBodyInLineFlag = 1 << 2; + + private readonly int _optionFlags; + private readonly string _indent; + private int _indentionLevel; + + public KnRJavascriptTextWriter(TextWriter writer, KnRJavascriptTextWriterOptions options) : base(writer, options) + { + if (!string.IsNullOrWhiteSpace(options.Indent)) + { + throw new ArgumentException("Indent must be null or white-space.", nameof(options)); + } + + _indent = options.Indent ?? " "; + + if (options.UseEgyptianBraces) + { + _optionFlags |= UseEgyptianBracesFlag; + } + + if (options.KeepSingleStatementBodyInLine) + { + _optionFlags |= KeepSingleStatementBodyInLineFlag; + } + + if (options.KeepEmptyBlockBodyInLine) + { + _optionFlags |= KeepEmptyBlockBodyInLineFlag; + } + + MultiLineArrayLiteralThreshold = options.MultiLineArrayLiteralThreshold >= 0 ? options.MultiLineArrayLiteralThreshold : int.MaxValue; + MultiLineObjectLiteralThreshold = options.MultiLineObjectLiteralThreshold >= 0 ? options.MultiLineObjectLiteralThreshold : int.MaxValue; + } + + protected bool UseEgyptianBraces { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (_optionFlags & UseEgyptianBracesFlag) != 0; } + protected bool KeepSingleStatementBodyInLine { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (_optionFlags & KeepSingleStatementBodyInLineFlag) != 0; } + protected bool KeepEmptyBlockBodyInLine { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (_optionFlags & KeepEmptyBlockBodyInLineFlag) != 0; } + protected int MultiLineArrayLiteralThreshold { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } + protected int MultiLineObjectLiteralThreshold { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } + + protected void IncreaseIndent() + { + _indentionLevel++; + } + + protected void DecreaseIndent() + { + _indentionLevel--; + } + + protected void WriteIndent() + { + for (var n = _indentionLevel; n > 0; n--) + { + WriteWhiteSpace(_indent); + } + } + + public override void WriteEpsilon(TokenFlags flags, ref WriteContext context) + { + if (WhiteSpaceWrittenSinceLastToken) + { + return; + } + + if ((flags & (TokenFlags.LeadingSpaceRecommended | TokenFlags.TrailingSpaceRecommended)) != 0) + { + ForceRecommendedSpace(); + } + } + + protected override void StartKeyword(string value, TokenFlags flags, ref WriteContext context) + { + if (WhiteSpaceWrittenSinceLastToken) + { + return; + } + + if (flags.HasFlagFast(TokenFlags.FollowsStatementBody)) + { + if (UseEgyptianBraces && CanUseEgyptianBraces(ref context)) + { + WriteSpace(); + } + else + { + WriteLine(); + WriteIndent(); + } + } + else if (flags.HasFlagFast(TokenFlags.LeadingSpaceRecommended) || LastTokenFlags.HasFlagFast(TokenFlags.TrailingSpaceRecommended)) + { + WriteSpace(); + } + else + { + base.StartKeyword(value, flags, ref context); + } + } + + protected override void StartIdentifier(string value, TokenFlags flags, ref WriteContext context) + { + if (WhiteSpaceWrittenSinceLastToken) + { + return; + } + + if (flags.HasFlagFast(TokenFlags.LeadingSpaceRecommended) || LastTokenFlags.HasFlagFast(TokenFlags.TrailingSpaceRecommended)) + { + WriteSpace(); + } + else + { + base.StartIdentifier(value, flags, ref context); + } + } + + protected override void StartLiteral(string value, TokenType type, TokenFlags flags, ref WriteContext context) + { + if (WhiteSpaceWrittenSinceLastToken) + { + return; + } + + if (flags.HasFlagFast(TokenFlags.LeadingSpaceRecommended) || LastTokenFlags.HasFlagFast(TokenFlags.TrailingSpaceRecommended)) + { + WriteSpace(); + } + else + { + base.StartLiteral(value, type, flags, ref context); + } + } + + protected override void StartPunctuator(string value, TokenFlags flags, ref WriteContext context) + { + if (WhiteSpaceWrittenSinceLastToken) + { + return; + } + + if (flags.HasFlagFast(TokenFlags.LeadingSpaceRecommended) || LastTokenFlags.HasFlagFast(TokenFlags.TrailingSpaceRecommended)) + { + WriteSpace(); + } + else + { + base.StartPunctuator(value, flags, ref context); + } + } + + public override void StartArray(int elementCount, ref WriteContext context) + { + base.StartArray(elementCount, ref context); + + if (context.Node.Type == Nodes.ArrayExpression && elementCount >= MultiLineArrayLiteralThreshold) + { + WriteLine(); + IncreaseIndent(); + } + } + + public override void EndArray(int elementCount, ref WriteContext context) + { + if (context.Node.Type == Nodes.ArrayExpression && elementCount >= MultiLineArrayLiteralThreshold) + { + DecreaseIndent(); + WriteIndent(); + } + + base.EndArray(elementCount, ref context); + } + + public override void StartObject(int propertyCount, ref WriteContext context) + { + base.StartObject(propertyCount, ref context); + + if (context.Node.Type == Nodes.ObjectExpression && propertyCount >= MultiLineObjectLiteralThreshold) + { + WriteLine(); + IncreaseIndent(); + } + } + + public override void EndObject(int propertyCount, ref WriteContext context) + { + if (context.Node.Type == Nodes.ObjectExpression && propertyCount >= MultiLineObjectLiteralThreshold) + { + DecreaseIndent(); + WriteIndent(); + } + + base.EndObject(propertyCount, ref context); + } + + public override void StartBlock(int statementCount, ref WriteContext context) + { + base.StartBlock(statementCount, ref context); + + if (statementCount > 0 || !KeepEmptyBlockBodyInLine) + { + WriteLine(); + IncreaseIndent(); + } + } + + public override void EndBlock(int statementCount, ref WriteContext context) + { + if (statementCount > 0 || !KeepEmptyBlockBodyInLine) + { + DecreaseIndent(); + WriteIndent(); + } + + base.EndBlock(statementCount, ref context); + } + + protected virtual void StoreStatementBodyIntoContext(Statement statement, ref WriteContext context) + { + context.Data = statement; + } + + protected virtual Statement RetrieveStatementBodyFromContext(ref WriteContext context) + { + return (Statement) (context.Data ?? throw new InvalidOperationException()); + } + + public override void StartStatement(StatementFlags flags, ref WriteContext context) + { + if (flags.HasFlagFast(StatementFlags.IsStatementBody)) + { + var statement = context.GetNodePropertyValue(); + StoreStatementBodyIntoContext(statement, ref context); + + // Is single statement body? + if (statement.Type != Nodes.BlockStatement) + { + if (CanInlineSingleStatementBody(statement, flags, ref context)) + { + WriteSpace(); + } + else + { + WriteLine(); + IncreaseIndent(); + WriteIndent(); + } + } + } + } + + public override void EndStatement(StatementFlags flags, ref WriteContext context) + { + if (flags.HasFlagFast(StatementFlags.IsStatementBody)) + { + var statement = RetrieveStatementBodyFromContext(ref context); + + // Is single statement body? + if (statement.Type != Nodes.BlockStatement) + { + if (!CanInlineSingleStatementBody(statement, flags, ref context)) + { + DecreaseIndent(); + } + } + } + + if (flags.HasFlagFast(StatementFlags.NeedsSemicolon) || ShouldTerminateStatementAnyway(context.GetNodePropertyValue(), flags, ref context)) + { + WritePunctuator(";", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref context); + } + } + + public override void StartStatementList(int count, ref WriteContext context) + { + if (context.Node.Type == Nodes.SwitchCase) + { + if (count == 1 && context.GetNodePropertyListValue()[0].Type == Nodes.BlockStatement) + { + WriteSpace(); + } + else + { + WriteLine(); + IncreaseIndent(); + } + } + } + + public override void StartStatementListItem(int index, int count, StatementFlags flags, ref WriteContext context) + { + if (context.Node.Type == Nodes.SwitchCase) + { + if (index == 0 && count == 1 && context.GetNodePropertyListValue()[0].Type == Nodes.BlockStatement) + { + return; + } + } + + WriteIndent(); + } + + public override void EndStatementListItem(int index, int count, StatementFlags flags, ref WriteContext context) + { + if (flags.HasFlagFast(StatementFlags.NeedsSemicolon) || ShouldTerminateStatementAnyway(context.GetNodePropertyListValue()[index], flags, ref context)) + { + WritePunctuator(";", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref context); + } + + WriteLine(); + } + + public override void EndStatementList(int count, ref WriteContext context) + { + if (context.Node.Type == Nodes.SwitchCase) + { + if (!(count == 1 && context.GetNodePropertyListValue()[0].Type == Nodes.BlockStatement)) + { + DecreaseIndent(); + } + } + } + + protected virtual bool CanUseEgyptianBraces(ref WriteContext context) + { + return KeepEmptyBlockBodyInLine + ? RetrieveStatementBodyFromContext(ref context) is BlockStatement blockStatement && blockStatement.Body.Count > 0 + : RetrieveStatementBodyFromContext(ref context).Type == Nodes.BlockStatement; + } + + protected virtual bool CanInlineSingleStatementBody(Statement statement, StatementFlags flags, ref WriteContext context) + { + return statement.Type switch + { + // Statements + Nodes.BreakStatement or + Nodes.ContinueStatement or + Nodes.DebuggerStatement or + Nodes.EmptyStatement or + Nodes.ExpressionStatement or + Nodes.ReturnStatement or + Nodes.ThrowStatement => + KeepSingleStatementBodyInLine, + + Nodes.BlockStatement or + Nodes.DoWhileStatement or + Nodes.ForInStatement or + Nodes.ForOfStatement or + Nodes.ForStatement or + Nodes.LabeledStatement or + Nodes.SwitchStatement or + Nodes.TryStatement or + Nodes.WhileStatement or + Nodes.WithStatement => + false, + + Nodes.IfStatement => + context is { Node: IfStatement, NodePropertyName: nameof(IfStatement.Alternate) }, + + // Declarations + Nodes.FunctionDeclaration or + Nodes.VariableDeclaration => + KeepSingleStatementBodyInLine, + + Nodes.ClassDeclaration or + Nodes.ImportDeclaration or + Nodes.ExportAllDeclaration or + Nodes.ExportDefaultDeclaration or + Nodes.ExportNamedDeclaration => + throw new ArgumentException($"Operation is not defined for nodes of type {statement.Type}.", nameof(statement)), + + // Extensions + _ => false, + }; + } + + protected virtual bool ShouldTerminateStatementAnyway(Statement statement, StatementFlags flags, ref WriteContext context) + { + return statement.Type switch + { + Nodes.DoWhileStatement => true, + _ => false + }; + } + + public override void StartExpressionListItem(int index, int count, ExpressionFlags flags, ref WriteContext context) + { + if (context.Node.Type == Nodes.ArrayExpression && count >= MultiLineArrayLiteralThreshold) + { + WriteIndent(); + } + + base.StartExpressionListItem(index, count, flags, ref context); + } + + public override void EndExpressionListItem(int index, int count, ExpressionFlags flags, ref WriteContext context) + { + base.EndExpressionListItem(index, count, flags, ref context); + + if (context.Node.Type == Nodes.ArrayExpression && count >= MultiLineArrayLiteralThreshold) + { + WriteLine(); + } + } + + public override void StartAuxiliaryNodeListItem(int index, int count, string separator, object? nodeContext, ref WriteContext context) + { + if (typeof(T) == typeof(SwitchCase) || + context.Node.Type == Nodes.ClassBody || + context.Node.Type == Nodes.ObjectExpression && count >= MultiLineObjectLiteralThreshold) + { + WriteIndent(); + } + } + + public override void EndAuxiliaryNodeListItem(int index, int count, string separator, object? nodeContext, ref WriteContext context) + { + base.EndAuxiliaryNodeListItem(index, count, separator, nodeContext, ref context); + + if (context.Node.Type is Nodes.ClassBody || + context.Node.Type == Nodes.ObjectExpression && count >= MultiLineObjectLiteralThreshold) + { + WriteLine(); + } + else if (typeof(T) == typeof(Decorator)) + { + WriteLine(); + WriteIndent(); + } + } +} diff --git a/src/Shared/Compatibility/NullableAttributes.cs b/src/Shared/Compatibility/NullableAttributes.cs new file mode 100644 index 00000000..b311a4dd --- /dev/null +++ b/src/Shared/Compatibility/NullableAttributes.cs @@ -0,0 +1,151 @@ +// Source: https://github.com/dotnet/runtime/blob/v6.0.5/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ + // These attributes already shipped with .NET Core 3.1 in System.Runtime +#if !NETCOREAPP3_0 && !NETCOREAPP3_1 && !NETSTANDARD2_1 + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + + internal sealed class AllowNullAttribute : Attribute + { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + + internal sealed class DisallowNullAttribute : Attribute + { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class MaybeNullAttribute : Attribute + { } + + /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullAttribute : Attribute + { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + internal sealed class DoesNotReturnAttribute : Attribute + { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } +#endif + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullAttribute : Attribute + { + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = new[] { member }; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } + } + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } + } +} diff --git a/test/Esprima.Tests/AstToJavascriptTests.cs b/test/Esprima.Tests/AstToJavascriptTests.cs new file mode 100644 index 00000000..51fc5706 --- /dev/null +++ b/test/Esprima.Tests/AstToJavascriptTests.cs @@ -0,0 +1,758 @@ +using System.Text.RegularExpressions; +using Esprima.Ast; +using Esprima.Test; +using Esprima.Utils; + +namespace Esprima.Tests +{ + public class AstToJavascriptTests + { + private record class CustomCompactJavascriptTextWriterOptions : JavascriptTextWriterOptions + { + protected internal override JavascriptTextWriter CreateWriter(TextWriter writer) => new CustomCompactJavascriptTextWriter(writer, this); + } + + private sealed class CustomCompactJavascriptTextWriter : JavascriptTextWriter + { + public CustomCompactJavascriptTextWriter(TextWriter writer, CustomCompactJavascriptTextWriterOptions options) : base(writer, options) { } + + public override void EndStatement(StatementFlags flags, ref WriteContext context) + { + if (flags.HasFlagFast(StatementFlags.NeedsSemicolon) || ShouldTerminateStatementAnyway(context.GetNodePropertyValue(), flags, ref context)) + { + WritePunctuator(";", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref context); + } + } + + public override void EndStatementListItem(int index, int count, StatementFlags flags, ref WriteContext context) + { + if (flags.HasFlagFast(StatementFlags.NeedsSemicolon) || ShouldTerminateStatementAnyway(context.GetNodePropertyListValue()[index], flags, ref context)) + { + WritePunctuator(";", TokenFlags.Trailing | TokenFlags.TrailingSpaceRecommended, ref context); + } + } + + private bool ShouldTerminateStatementAnyway(Statement statement, StatementFlags flags, ref WriteContext context) + { + return statement.Type switch + { + Nodes.DoWhileStatement => true, + _ => false + }; + } + } + + private static readonly CustomCompactJavascriptTextWriterOptions s_customCompactWriterOptions = new(); + private static readonly KnRJavascriptTextWriterOptions s_indentedWriterOptions = new() + { + Indent = " ", + KeepEmptyBlockBodyInLine = false, + MultiLineObjectLiteralThreshold = 1 + }; + + [Fact] + public void ToJavascriptTest1() + { + var parser = new JavaScriptParser(@"if (true) { p(); } +switch(foo) { + case 'A': + p(); + break; +} +switch(foo) { + default: + p(); + break; +} +for (var a = []; ; ) { } +for (var elem of list) { } +"); + var program = parser.ParseScript(); + + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + + Assert.Equal("if(true){p();}switch(foo){case'A':p();break;}switch(foo){default:p();break;}for(var a=[];;){}for(var elem of list){}", code); + } + + [Fact] + public void ToJavascriptTest2() + { + var parser = new JavaScriptParser(@"let tips = [ + ""Click on any AST node with a '+' to expand it"", + + ""Hovering over a node highlights the \ + corresponding location in the source code"", + + ""Shift click on an AST node to expand the whole subtree"" +]; + + function printTips() + { + tips.forEach((tip, i) => console.log(`Tip ${ i}:` +tip)); + }"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("let tips=[\"Click on any AST node with a '+' to expand it\",\"Hovering over a node highlights the \\\r\n corresponding location in the source code\",\"Shift click on an AST node to expand the whole subtree\"];function printTips(){tips.forEach((tip,i)=>console.log(`Tip ${i}:`+tip));}", code); + } + + [Fact] + public void ToJavascriptTest3() + { + var parser = new JavaScriptParser(@"export class aa extends HTMLElement{ + constructor(a, b) + { + super(a); + this._div = document.createElement('div'); + } + static get is() { + return 'aa'; + } +}"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("export class aa extends HTMLElement{constructor(a,b){super(a);this._div=document.createElement('div');}static get is(){return'aa';}}", code); + } + + [Fact] + public void ToJavascriptTest4() + { + var source = @"import { MccDialog } from '../mccDialogHandler'; +import { commonClient, bb as f } from '../commonClient/commonClient'; +import ii, { hh, jj } from '../commonClient/commonClient'; +import '../commonClient/commonClient'; +import aa from 'module-name'; +import zz, * as ff from 'module-name'; +import * as name from 'module-name'; +import('qq'); +a++; +--a; +export function checkSecurityAnswerCodeDirect(result) { + if (!result) { + MccDialog.warning({ + title: 'SecurityClientErrorOccured', + message: '

internal error, check console

', + }); + return false; + } + switch (result.SecurityAnswerCode) { + case 'Allowed': + return true; + case 'Exception': + MccDialog.warning({ + title: 'SecurityClientInfoTitle', + message: '

SecurityClientExceptionOccured

Exception: ' + result.Message + '

' + result.StackTrace, + }); + return false; + case 'Error': + MccDialog.warning({ + title: 'SecurityClientErrorOccured', + message: '

' + + commonClient.getTranslation('SecurityClientMessage') + + ': ' + + commonClient.getTranslation(result.Message) + + '

' + + (result.MessageDetails ? '

SecurityClientDetails: ' + result.MessageDetails + '

' : ' '), + }); + return false; + default: { + let messagesnippet = '

SecurityClient_' + result.SecurityAnswerCode + '

'; + if (result.Message !== undefined && result.SecurityAnswerCode === 'LoginFailed') { + messagesnippet += '\n\nSecurityClient_InternalServerErrorMessage\n' + result.Message + ''; + } + if (result.Role) { + messagesnippet += '

SecurityClient_CheckedRole' + ' [' + result.Role + ']' + '

'; + } + MccDialog.warning({ + title: 'SecurityClientInfoTitle', + message: messagesnippet, + }); + return false; + } + } +}"; + source = Regex.Replace(source, @"\r\n|\n\r|\n|\r", Environment.NewLine); + var parser = new JavaScriptParser(source); + var program = parser.ParseScript(); + var code = AstToJavascript.ToJavascriptString(program, s_indentedWriterOptions); + + var expected = @"import { MccDialog } from '../mccDialogHandler'; +import { commonClient, bb as f } from '../commonClient/commonClient'; +import ii, { hh, jj } from '../commonClient/commonClient'; +import '../commonClient/commonClient'; +import aa from 'module-name'; +import zz, * as ff from 'module-name'; +import * as name from 'module-name'; +import('qq'); +a++; +--a; +export function checkSecurityAnswerCodeDirect(result) { + if (!result) { + MccDialog.warning({ + title: 'SecurityClientErrorOccured', + message: '

internal error, check console

' + }); + return false; + } + switch (result.SecurityAnswerCode) { + case 'Allowed': + return true; + case 'Exception': + MccDialog.warning({ + title: 'SecurityClientInfoTitle', + message: '

SecurityClientExceptionOccured

Exception: ' + result.Message + '

' + result.StackTrace + }); + return false; + case 'Error': + MccDialog.warning({ + title: 'SecurityClientErrorOccured', + message: '

' + commonClient.getTranslation('SecurityClientMessage') + ': ' + commonClient.getTranslation(result.Message) + '

' + (result.MessageDetails ? '

SecurityClientDetails: ' + result.MessageDetails + '

' : ' ') + }); + return false; + default: { + let messagesnippet = '

SecurityClient_' + result.SecurityAnswerCode + '

'; + if (result.Message !== undefined && result.SecurityAnswerCode === 'LoginFailed') { + messagesnippet += '\n\nSecurityClient_InternalServerErrorMessage\n' + result.Message + ''; + } + if (result.Role) { + messagesnippet += '

SecurityClient_CheckedRole' + ' [' + result.Role + ']' + '

'; + } + MccDialog.warning({ + title: 'SecurityClientInfoTitle', + message: messagesnippet + }); + return false; + } + } +} +"; + expected = Regex.Replace(expected, @"\r\n|\n\r|\n|\r", Environment.NewLine); + Assert.Equal(expected, code); + } + + [Fact] + public void ToJavascriptTest5() + { + var source = @"(function () { + 'use strict'; +})(); + +(class ApplyShimInterface { + constructor() { + this.customStyleInterface = null; + applyShim['invalidCallback'] = ApplyShimUtils.invalidate; + } +}); + +( + a +)(); + + +aa({}); + +(function aa(){});"; + source = Regex.Replace(source, @"\r\n|\n\r|\n|\r", Environment.NewLine); + var parser = new JavaScriptParser(source); + var program = parser.ParseScript(); + var code = AstToJavascript.ToJavascriptString(program, s_indentedWriterOptions); + + var expected = @"(function() { + 'use strict'; +})(); +(class ApplyShimInterface { + constructor() { + this.customStyleInterface = null; + applyShim['invalidCallback'] = ApplyShimUtils.invalidate; + } +}); +a(); +aa({ }); +(function aa() { +}); +"; + expected = Regex.Replace(expected, @"\r\n|\n\r|\n|\r", Environment.NewLine); + Assert.Equal(expected, code); + } + + [Fact] + public void ToJavascriptTest6() + { + var source = @"function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; + }"; + source = Regex.Replace(source, @"\r\n|\n\r|\n|\r", Environment.NewLine); + var parser = new JavaScriptParser(source); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("function _createClass(Constructor,protoProps,staticProps){if(protoProps)_defineProperties(Constructor.prototype,protoProps);if(staticProps)_defineProperties(Constructor,staticProps);return Constructor;}", code); + } + + [Fact] + public void ToJavascriptTest7() + { + var parser = new JavaScriptParser(@"if ((x ? a.nodeName.toLowerCase() === f : 1 === a.nodeType) && ++d && (p && ((i = (o = a[S] || (a[S] = {}))[a.uniqueID] || (o[a.uniqueID] = {}))[h] = [k, d]), a === e)) +{ +}"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("if((x?a.nodeName.toLowerCase()===f:1===a.nodeType)&&++d&&(p&&((i=(o=a[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]=[k,d]),a===e)){}", code); + } + + [Fact] + public void ToJavascriptTest8() + { + var parser = new JavaScriptParser(@" +class a extends b { + constructor() { + super(); + this.g=1; + } + + q=1; + r='cc'; +} +"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("class a extends b{constructor(){super();this.g=1;}q=1;r='cc';}", code); + } + + [Fact] + public void ToJavascriptTest9() + { + var parser = new JavaScriptParser(@" +d = (s = (r = (i = (o = (a = c)[S] || (a[S] = {}))[a.uniqueID] || (o[a.uniqueID] = {}))[h] || [])[0] === k && r[1]) && r[2], a = s && c.childNodes[s]; +"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("d=(s=(r=(i=(o=(a=c)[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]||[])[0]===k&&r[1])&&r[2],a=s&&c.childNodes[s];", code); + } + + [Fact] + public void ToJavascriptTest10() + { + var parser = new JavaScriptParser(@" +m = (z.document, !!v.documentElement && !!v.head && 'function' == typeof v.addEventListener && v.createElement, ~a.indexOf('MSIE') || a.indexOf('Trident/'), '___FONT_AWESOME___') +"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("m=(z.document,!!v.documentElement&&!!v.head&&'function'==typeof v.addEventListener&&v.createElement,~a.indexOf('MSIE')||a.indexOf('Trident/'),'___FONT_AWESOME___');", code); + } + + [Fact] + public void ToJavascriptTest11() + { + var parser = new JavaScriptParser(@" + var h = (c.navigator || {}).userAgent, + a = void 0 === h ? '' : h, + z = c, + v = l, + m = (z.document, !!v.documentElement && !!v.head && 'function' == typeof v.addEventListener && v.createElement, ~a.indexOf('MSIE') || a.indexOf('Trident/'), '___FONT_AWESOME___'), + e = function() { + try { + return !0 + } catch (c) { + return !1 + } + }(); +"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("var h=(c.navigator||{}).userAgent,a=void 0===h?'':h,z=c,v=l,m=(z.document,!!v.documentElement&&!!v.head&&'function'==typeof v.addEventListener&&v.createElement,~a.indexOf('MSIE')||a.indexOf('Trident/'),'___FONT_AWESOME___'),e=function(){try{return!0;}catch(c){return!1;}}();", code); + } + + [Fact] + public void ToJavascriptTest12() + { + var parser = new JavaScriptParser(@" +var a = { +children: (b = O, 'g' === b.tag ? b.children : [b]) +} +"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("var a={children:(b=O,'g'===b.tag?b.children:[b])};", code); + } + + [Fact] + public void ToJavascriptTest13() + { + var parser = new JavaScriptParser(@" +if (e.IsWebService) + if (h = e.HttpRequest.responseXML, 'undefined' == typeof h) Trace.Write('Error: ' + e.UniqueId + ' data has no properties!'), m = !0; + else try { + h.setProperty('SelectionLanguage', 'XPath') + } catch (l) { + Trace.Write('Error: data.setProperty('SelectionLanguage', 'XPath') because ' + l.message) + } else h = e.HttpRequest.responseText; +"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("if(e.IsWebService)if(h=e.HttpRequest.responseXML,'undefined'==typeof h)Trace.Write('Error: '+e.UniqueId+' data has no properties!'),m=!0;else try{h.setProperty('SelectionLanguage','XPath');}catch(l){Trace.Write('Error: data.setProperty(',SelectionLanguage,', ',XPath,') because '+l.message);}else h=e.HttpRequest.responseText;", code); + } + + [Fact] + public void ToJavascriptTest14() + { + var source = @"function tt(t, r) { + var n, e, i = b(t), + s = b(r); + if (s && (e = ft(r)), i); + else if (s) return D(t, e) ? void $(t, e) : (n = l(e, t), G(t, n), void ht(t)); + var g, o, f; + for (f = t.length < r.length ? t.length : r.length, o = 0, g = 0; f > g; g++) o += t[g] + r[g], t[g] = o & _t, o >>= at; + for (g = f; o && g < t.length; g++) o += t[g], t[g] = o & _t, o >>= at +} +"; + source = Regex.Replace(source, @"\r\n|\n\r|\n|\r", Environment.NewLine); + var parser = new JavaScriptParser(source); + var program = parser.ParseScript(); + var code = AstToJavascript.ToJavascriptString(program, s_indentedWriterOptions); + + var expected = @"function tt(t, r) { + var n, e, i = b(t), s = b(r); + if (s && (e = ft(r)), i) + ; + else if (s) + return D(t, e) ? void $(t, e) : (n = l(e, t), G(t, n), void ht(t)); + var g, o, f; + for (f = t.length < r.length ? t.length : r.length, o = 0, g = 0; f > g; g++) + o += t[g] + r[g], t[g] = o & _t, o >>= at; + for (g = f; o && g < t.length; g++) + o += t[g], t[g] = o & _t, o >>= at; +} +"; + expected = Regex.Replace(expected, @"\r\n|\n\r|\n|\r", Environment.NewLine); + Assert.Equal(expected, code); + } + + [Fact] + public void ToJavascriptTest15() + { + var parser = new JavaScriptParser(@" +h='M'+(+new Date).toString(36) +"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("h='M'+(+new Date).toString(36);", code); + } + + [Fact] + public void ToJavascriptTest16() + { + var parser = new JavaScriptParser(@" +input.onchange = async (e) => { + const files = await readFiles(input.files, readMode); + document.body.removeChild(input); + resolve(files); + }; +"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("input.onchange=async e=>{const files=await readFiles(input.files,readMode);document.body.removeChild(input);resolve(files);};", code); + } + + [Fact] + public void ToJavascriptTest17() + { + var parser = new JavaScriptParser(@" +export const Base = LegacyElementMixin(HTMLElement).prototype; +"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("export const Base=LegacyElementMixin(HTMLElement).prototype;", code); + } + + [Fact] + public void ToJavascriptTest18() + { + var parser = new JavaScriptParser(@" +let {is} = getIsExtends(element); +"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("let{is}=getIsExtends(element);", code); + } + + [Fact] + public void ToJavascriptTest19() + { + var parser = new JavaScriptParser(@" +export const wrap = + (window['ShadyDOM'] && window['ShadyDOM']['wrap']) || (node => node); +"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("export const wrap=window['ShadyDOM']&&window['ShadyDOM']['wrap']||(node=>node);", code); + } + + [Fact] + public void ToJavascriptTest20() + { + var parser = new JavaScriptParser(@" +export {}"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("export{};", code); + } + + [Fact] + public void ToJavascriptTest21() + { + var parser = new JavaScriptParser(@" +(() => { + mutablePropertyChange = MutableData._mutablePropertyChange; +})(); +"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("(()=>{mutablePropertyChange=MutableData._mutablePropertyChange;})();", code); + } + + [Fact] + public void ToJavascriptTest22() + { + var parser = new JavaScriptParser(@" +var Ol, jl = new (function() { + var l, h, z; + return l = c + }()) +"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("var Ol,jl=new(function(){var l,h,z;return l=c;}());", code); + } + + [Fact] + public void ToJavascriptTest23() + { + var parser = new JavaScriptParser(@" + +[y, { + [Symbol.iterator]() { + return b + },a:5 + }] + +"); + var program = parser.ParseScript(); + var code = program.ToJavascriptString(s_customCompactWriterOptions, AstToJavascriptOptions.Default); + Assert.Equal("[y,{[Symbol.iterator](){return b;},a:5}];", code); + } + + [Fact] + public void ToJavascriptTest24() + { + var source = @" + +class A { +*[Symbol.iterator]() { + let L = this._first; + for (; L !== _.Undefined; ) + yield L.element, + L = L.next + } +} + +"; + source = Regex.Replace(source, @"\r\n|\n\r|\n|\r", Environment.NewLine); + var parser = new JavaScriptParser(source); + var program = parser.ParseScript(); + var code = AstToJavascript.ToJavascriptString(program, s_indentedWriterOptions); + + var expected = @"class A { + *[Symbol.iterator]() { + let L = this._first; + for (; L !== _.Undefined; ) + yield L.element, L = L.next; + } +} +"; + expected = Regex.Replace(expected, @"\r\n|\n\r|\n|\r", Environment.NewLine); + Assert.Equal(expected, code); + } + + [Fact] + public void ToJavascriptTest25() + { + var source = @"var i = function e(i) { + var r = n[i]; + if (void 0 !== r) + return r.exports; + var a = n[i] = { + exports: {} + }; + return t[i](a, a.exports, e), + a.exports + }(15); +"; + source = Regex.Replace(source, @"\r\n|\n\r|\n|\r", Environment.NewLine); + var parser = new JavaScriptParser(source); + var program = parser.ParseScript(); + var code = AstToJavascript.ToJavascriptString(program, s_indentedWriterOptions); + + var expected = @"var i = function e(i) { + var r = n[i]; + if (void 0 !== r) + return r.exports; + var a = n[i] = { + exports: { } + }; + return t[i](a, a.exports, e), a.exports; +}(15); +"; + expected = Regex.Replace(expected, @"\r\n|\n\r|\n|\r", Environment.NewLine); + Assert.Equal(expected, code); + } + + [Fact] + public void ToJavascriptTest26() + { + var source = @"class A { + aa() { + let a = 1; + } +} +var b = 1; +var c; +if (b == 2) { + c = 1; +} else { + c = 3; +} +"; + source = Regex.Replace(source, @"\r\n|\n\r|\n|\r", Environment.NewLine); + var parser = new JavaScriptParser(source); + var program = parser.ParseScript(); + var code = AstToJavascript.ToJavascriptString(program, s_indentedWriterOptions); + Assert.Equal(source, code); + } + + private sealed class NodeTypeEqualityComparer : IEqualityComparer + { + public static NodeTypeEqualityComparer Default = new NodeTypeEqualityComparer(); + + public bool Equals(Node? x, Node? y) => + x is null && y is null ? true : + x is null || y is null ? false : + x.Type == y.Type; + + public int GetHashCode(Node? obj) => obj?.GetHashCode() ?? 0; + } + + // TODO: this should be removed once the related parser bugs get resolved + private static readonly HashSet s_falseNegatives = new() + { + @"es2017\async\methods\async-line-terminator-method.js", + @"es2017\async\methods\async-line-terminator-static-method.js", + @"es2017\async\arrows\export-default-async-arrow.module.js" + }; + + public static IEnumerable SourceFiles(string relativePath) => Fixtures.SourceFiles(relativePath) + // TODO: enable JSX fixtures once JSX writer gets implemented + .Where(items => !((string) items[0]).StartsWith("JSX")) + .Where(items => !s_falseNegatives.Contains(((string) items[0]).Replace('/', '\\'))); + + private static Program Parse(SourceType sourceType, string source, + ParserOptions parserOptions, Func parserFactory) + { + var parser = parserFactory(source, parserOptions); + var program = sourceType == SourceType.Script ? (Program) parser.ParseScript() : parser.ParseModule(); + + return program; + } + + [Theory] + [MemberData(nameof(SourceFiles), "Fixtures")] + public void OriginalAndReparsedASTsShouldMatch(string fixture) + { + var (parserOptions, parserFactory) = fixture.StartsWith("JSX") + ? (new JsxParserOptions(), + (src, opts) => new JsxParser(src, (JsxParserOptions) opts)) + : (new ParserOptions(), + new Func((src, opts) => new JavaScriptParser(src, opts))); + + parserOptions.Tokens = false; + parserOptions.AdaptRegexp = false; + parserOptions.Tolerant = false; + + string treeFilePath, failureFilePath, moduleFilePath; + var jsFilePath = Path.Combine(Fixtures.GetFixturesPath(), Fixtures.FixturesDirName, fixture); + var jsFileDirectoryName = Path.GetDirectoryName(jsFilePath)!; + if (jsFilePath.EndsWith(".source.js")) + { + treeFilePath = Path.Combine(jsFileDirectoryName, Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(jsFilePath))) + ".tree.json"; + failureFilePath = Path.Combine(jsFileDirectoryName, Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(jsFilePath))) + ".failure.json"; + moduleFilePath = Path.Combine(jsFileDirectoryName, Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(jsFilePath))) + ".module.json"; + } + else + { + treeFilePath = Path.Combine(jsFileDirectoryName, Path.GetFileNameWithoutExtension(jsFilePath)) + ".tree.json"; + failureFilePath = Path.Combine(jsFileDirectoryName, Path.GetFileNameWithoutExtension(jsFilePath)) + ".failure.json"; + moduleFilePath = Path.Combine(jsFileDirectoryName, Path.GetFileNameWithoutExtension(jsFilePath)) + ".module.json"; + } + + var script = File.ReadAllText(jsFilePath); + if (jsFilePath.EndsWith(".source.js")) + { + var parser = new JavaScriptParser(script); + var program = parser.ParseScript(); + var source = program.Body.First().As().Declarations.First().As().Init!.As().StringValue!; + script = source; + } + + var filename = Path.GetFileNameWithoutExtension(jsFilePath); + + if (filename.Contains("error") || + filename.Contains("invalid") && (!filename.Contains("invalid-yield-object-") && !filename.Contains("attribute-invalid-entity"))) + { + return; + } + + var isModule = + filename.Contains("module") || + filename.Contains("export") || + filename.Contains("import"); + + if (!filename.Contains(".module")) + { + isModule &= !jsFilePath.Contains("dynamic-import") && !jsFilePath.Contains("script"); + } + + var sourceType = isModule + ? SourceType.Module + : SourceType.Script; + + Program expectedAst; + if (File.Exists(moduleFilePath)) + { + sourceType = SourceType.Module; + } + else if (!File.Exists(treeFilePath)) + { + return; + } + + try { expectedAst = Parse(sourceType, script, parserOptions, parserFactory); } + catch (ParserException) { return; } + + var generatedScript = expectedAst.ToJavascriptString(); + + var actualAst = Parse(sourceType, generatedScript, parserOptions, parserFactory); + + // This compares just the node type. + // TODO: more detailed comparison. + Assert.Equal(expectedAst.DescendantNodesAndSelf(), actualAst.DescendantNodesAndSelf(), NodeTypeEqualityComparer.Default); + + generatedScript = expectedAst.ToJavascriptString(beautify: true); + + actualAst = Parse(sourceType, generatedScript, parserOptions, parserFactory); + + // This compares just the node type. + // TODO: more detailed comparison. + Assert.Equal(expectedAst.DescendantNodesAndSelf(), actualAst.DescendantNodesAndSelf(), NodeTypeEqualityComparer.Default); + } + } +} diff --git a/test/Esprima.Tests/VisitorTests.cs b/test/Esprima.Tests/AstVisitorTests.cs similarity index 98% rename from test/Esprima.Tests/VisitorTests.cs rename to test/Esprima.Tests/AstVisitorTests.cs index d114cb51..74e0e18c 100644 --- a/test/Esprima.Tests/VisitorTests.cs +++ b/test/Esprima.Tests/AstVisitorTests.cs @@ -7,7 +7,7 @@ namespace Esprima.Tests { - public class VisitorTests + public class AstVisitorTests { [Fact] public void CanVisitIfWithNoElse() diff --git a/test/Esprima.Tests/Fixtures.cs b/test/Esprima.Tests/Fixtures.cs index 34ae467b..db59ece1 100644 --- a/test/Esprima.Tests/Fixtures.cs +++ b/test/Esprima.Tests/Fixtures.cs @@ -13,27 +13,18 @@ public class Fixtures // Only use this when the test is deemed wrong. private const bool WriteBackExpectedTree = false; - private const string FixturesDirName = "Fixtures"; + internal const string FixturesDirName = "Fixtures"; private static Lazy> Metadata { get; } = new(() => FixtureMetadata.ReadMetadata()); - [Fact] - public void HoistingScopeShouldWork() - { - var parser = new JavaScriptParser(@" - function p() {} - var x;"); - var program = parser.ParseScript(); - } - private static string ParseAndFormat(SourceType sourceType, string source, ParserOptions parserOptions, Func parserFactory, - AstJson.IConverter converter, AstJson.Options conversionOptions) + AstToJsonOptions conversionOptions) { var parser = parserFactory(source, parserOptions); var program = sourceType == SourceType.Script ? (Program) parser.ParseScript() : parser.ParseModule(); - return program.ToJsonString(conversionOptions, indent: " ", converter); + return program.ToJsonString(conversionOptions, indent: " "); } private static bool CompareTreesInternal(JObject actualJObject, JObject expectedJObject, FixtureMetadata metadata) @@ -87,13 +78,13 @@ private static void CompareTreesAndAssert(string actual, string expected, Fixtur [MemberData(nameof(SourceFiles), "Fixtures")] public void ExecuteTestCase(string fixture) { - var (parserOptions, parserFactory, converter) = fixture.StartsWith("JSX") + var (parserOptions, parserFactory, conversionDefaultOptions) = fixture.StartsWith("JSX") ? (new JsxParserOptions(), (src, opts) => new JsxParser(src, (JsxParserOptions) opts), - JsxAstToJsonConverter.Default) + JsxAstToJsonOptions.Default) : (new ParserOptions(), new Func((src, opts) => new JavaScriptParser(src, opts)), - AstToJsonConverter.Default); + AstToJsonOptions.Default); parserOptions.Tokens = true; @@ -148,14 +139,16 @@ public void ExecuteTestCase(string fixture) parserOptions.AdaptRegexp = !metadata.IgnoresRegex; + var conversionOptions = metadata.CreateConversionOptions(conversionDefaultOptions); + #pragma warning disable 162 if (File.Exists(moduleFilePath)) { sourceType = SourceType.Module; expected = File.ReadAllText(moduleFilePath); - if (WriteBackExpectedTree && !metadata.ConversionOptions.TestCompatibilityMode) + if (WriteBackExpectedTree && conversionOptions.TestCompatibilityMode == AstToJsonTestCompatibilityMode.None) { - var actual = ParseAndFormat(sourceType, script, parserOptions, parserFactory, converter, metadata.ConversionOptions); + var actual = ParseAndFormat(sourceType, script, parserOptions, parserFactory, conversionOptions); if (!CompareTrees(actual, expected, metadata)) File.WriteAllText(moduleFilePath, actual); } @@ -163,9 +156,9 @@ public void ExecuteTestCase(string fixture) else if (File.Exists(treeFilePath)) { expected = File.ReadAllText(treeFilePath); - if (WriteBackExpectedTree && !metadata.ConversionOptions.TestCompatibilityMode) + if (WriteBackExpectedTree && conversionOptions.TestCompatibilityMode == AstToJsonTestCompatibilityMode.None) { - var actual = ParseAndFormat(sourceType, script, parserOptions, parserFactory, converter, metadata.ConversionOptions); + var actual = ParseAndFormat(sourceType, script, parserOptions, parserFactory, conversionOptions); if (!CompareTrees(actual, expected, metadata)) File.WriteAllText(treeFilePath, actual); } @@ -174,9 +167,9 @@ public void ExecuteTestCase(string fixture) { invalid = true; expected = File.ReadAllText(failureFilePath); - if (WriteBackExpectedTree && !metadata.ConversionOptions.TestCompatibilityMode) + if (WriteBackExpectedTree && conversionOptions.TestCompatibilityMode == AstToJsonTestCompatibilityMode.None) { - var actual = ParseAndFormat(sourceType, script, parserOptions, parserFactory, converter, metadata.ConversionOptions); + var actual = ParseAndFormat(sourceType, script, parserOptions, parserFactory, conversionOptions); if (!CompareTrees(actual, expected, metadata)) File.WriteAllText(failureFilePath, actual); } @@ -196,7 +189,7 @@ public void ExecuteTestCase(string fixture) { parserOptions.Tolerant = true; - var actual = ParseAndFormat(sourceType, script, parserOptions, parserFactory, converter, metadata.ConversionOptions); + var actual = ParseAndFormat(sourceType, script, parserOptions, parserFactory, conversionOptions); CompareTreesAndAssert(actual, expected, metadata); } else @@ -204,7 +197,7 @@ public void ExecuteTestCase(string fixture) parserOptions.Tolerant = false; // TODO: check the accuracy of the message and of the location - Assert.Throws(() => ParseAndFormat(sourceType, script, parserOptions, parserFactory, converter, metadata.ConversionOptions)); + Assert.Throws(() => ParseAndFormat(sourceType, script, parserOptions, parserFactory, conversionOptions)); } } @@ -232,59 +225,12 @@ internal static string GetFixturesPath() return root ?? ""; } - private sealed class ParentNodeChecker : AstVisitor - { - public void Check(Node node) - { - Assert.Null(node.Data); - - base.Visit(node); - } - - public override object? Visit(Node node) - { - var parent = (Node?) node.Data; - Assert.NotNull(parent); - Assert.Contains(node, parent!.ChildNodes); - - return base.Visit(node); - } - } - - [Fact] - public void NodeDataCanBeSetToParentNode() - { - Action action = node => - { - foreach (var child in node.ChildNodes) - { - child.Data = node; - } - }; - - var parser = new JavaScriptParser("function add(a, b) { return a + b; }", new ParserOptions { OnNodeCreated = action }); - var script = parser.ParseScript(); - - new ParentNodeChecker().Check(script); - } - - [Fact] - public void CommentsAreParsed() - { - var count = 0; - Action action = node => count++; - var parser = new JavaScriptParser("// this is a comment", new ParserOptions { OnNodeCreated = action }); - parser.ParseScript(); - - Assert.Equal(1, count); - } - private sealed class FixtureMetadata { public static readonly FixtureMetadata Default = new FixtureMetadata( - AstJson.Options.Default - .WithIncludingLineColumn(true) - .WithIncludingRange(true), + testCompatibilityMode: AstToJsonTestCompatibilityMode.None, + includesLocation: true, + includesRange: true, includesLocationSource: false, ignoresRegex: false); @@ -321,33 +267,35 @@ public static Dictionary ReadMetadata() private static FixtureMetadata CreateFrom(HashSet flags) { - var conversionOptions = AstJson.Options.Default; - - if (flags.Contains("IncludesLocation")) - conversionOptions = conversionOptions.WithIncludingLineColumn(true); - - if (flags.Contains("IncludesRange")) - conversionOptions = conversionOptions.WithIncludingRange(true); - - if (flags.Contains("BorrowedFixture")) - conversionOptions = conversionOptions.WithTestCompatibilityMode(true); - - var includesLocationSource = flags.Contains("IncludesLocationSource"); - var ignoresRegex = flags.Contains("IgnoresRegex"); - - return new FixtureMetadata(conversionOptions, includesLocationSource, ignoresRegex); + return new FixtureMetadata( + testCompatibilityMode: flags.Contains("EsprimaOrgFixture") ? AstToJsonTestCompatibilityMode.EsprimaOrg : AstToJsonTestCompatibilityMode.None, + includesLocation: flags.Contains("IncludesLocation"), + includesRange: flags.Contains("IncludesRange"), + includesLocationSource: flags.Contains("IncludesLocationSource"), + ignoresRegex: flags.Contains("IgnoresRegex")); } - private FixtureMetadata(AstJson.Options conversionOptions, bool includesLocationSource, bool ignoresRegex) + private FixtureMetadata(AstToJsonTestCompatibilityMode testCompatibilityMode, bool includesLocation, bool includesRange, bool includesLocationSource, bool ignoresRegex) { - ConversionOptions = conversionOptions; + TestCompatibilityMode = testCompatibilityMode; + IncludesLocation = includesLocation; + IncludesRange = includesRange; IncludesLocationSource = includesLocationSource; IgnoresRegex = ignoresRegex; } - public AstJson.Options ConversionOptions { get; } + public AstToJsonTestCompatibilityMode TestCompatibilityMode { get; } + public bool IncludesLocation { get; } + public bool IncludesRange { get; } public bool IncludesLocationSource { get; } public bool IgnoresRegex { get; } + + public AstToJsonOptions CreateConversionOptions(AstToJsonOptions defaultOptions) => defaultOptions with + { + TestCompatibilityMode = TestCompatibilityMode, + IncludingLineColumn = IncludesLocation, + IncludingRange = IncludesRange, + }; } } } diff --git a/test/Esprima.Tests/Fixtures/fixtures-metadata.json b/test/Esprima.Tests/Fixtures/fixtures-metadata.json index 78ebd2f5..dca44489 100644 --- a/test/Esprima.Tests/Fixtures/fixtures-metadata.json +++ b/test/Esprima.Tests/Fixtures/fixtures-metadata.json @@ -10,7 +10,7 @@ [ { - "flags": [ "BorrowedFixture", "IncludesLocation", "IncludesRange" ], + "flags": [ "EsprimaOrgFixture", "IncludesLocation", "IncludesRange" ], "files": [ "3rdparty/angular-1.2.5.js", "3rdparty/angular-1.7.9.js", @@ -1683,13 +1683,13 @@ ] }, { - "flags": [ "BorrowedFixture", "IncludesLocation", "IncludesRange", "IncludesLocationSource" ], + "flags": [ "EsprimaOrgFixture", "IncludesLocation", "IncludesRange", "IncludesLocationSource" ], "files": [ "expression/complex/migrated_0001.js" ] }, { - "flags": [ "BorrowedFixture", "IncludesLocation", "IncludesRange", "IgnoresRegex" ], + "flags": [ "EsprimaOrgFixture", "IncludesLocation", "IncludesRange", "IgnoresRegex" ], "files": [ "expression/primary/literal/regular-expression/migrated_0003.js", "expression/primary/literal/regular-expression/migrated_0004.js", @@ -1698,14 +1698,14 @@ ] }, { - "flags": [ "BorrowedFixture", "IncludesRange" ], + "flags": [ "EsprimaOrgFixture", "IncludesRange" ], "files": [ "expression/primary/literal/numeric/migrated_0002.js", "expression/primary/literal/regular-expression/migrated_0007.js" ] }, { - "flags": [ "BorrowedFixture", "IncludesLocation" ], + "flags": [ "EsprimaOrgFixture", "IncludesLocation" ], "files": [ "expression/primary/literal/numeric/migrated_0003.js", "expression/primary/literal/regular-expression/migrated_0008.js" diff --git a/test/Esprima.Tests/ParserTests.cs b/test/Esprima.Tests/ParserTests.cs index 8e5db31e..a396b9a0 100644 --- a/test/Esprima.Tests/ParserTests.cs +++ b/test/Esprima.Tests/ParserTests.cs @@ -1,5 +1,6 @@ using Esprima.Ast; using Esprima.Test; +using Esprima.Utils; namespace Esprima.Tests { @@ -368,5 +369,61 @@ public void TemplateLiteralChildNodesShouldCorrectOrder(string source, params st return string.Empty; } } + + [Fact] + public void HoistingScopeShouldWork() + { + var parser = new JavaScriptParser(@" + function p() {} + var x;"); + var program = parser.ParseScript(); + } + + private sealed class ParentNodeChecker : AstVisitor + { + public void Check(Node node) + { + Assert.Null(node.Data); + + base.Visit(node); + } + + public override object? Visit(Node node) + { + var parent = (Node?) node.Data; + Assert.NotNull(parent); + Assert.Contains(node, parent!.ChildNodes); + + return base.Visit(node); + } + } + + [Fact] + public void NodeDataCanBeSetToParentNode() + { + Action action = node => + { + foreach (var child in node.ChildNodes) + { + child.Data = node; + } + }; + + var parser = new JavaScriptParser("function add(a, b) { return a + b; }", new ParserOptions { OnNodeCreated = action }); + var script = parser.ParseScript(); + + new ParentNodeChecker().Check(script); + } + + [Fact] + public void CommentsAreParsed() + { + var count = 0; + Action action = node => count++; + var parser = new JavaScriptParser("// this is a comment", new ParserOptions { OnNodeCreated = action }); + parser.ParseScript(); + + Assert.Equal(1, count); + } } }