Skip to content

Commit 93ddbe6

Browse files
authored
Fix parsing of optional chaining in new expressions (#393)
* Implement check for optional chaining syntax error in new expressions * Fix decorator parsing and formatting in AST to JS conversion * Minor code style and XML doc corrections
1 parent d29cb04 commit 93ddbe6

File tree

37 files changed

+3189
-1922
lines changed

37 files changed

+3189
-1922
lines changed

src/Esprima/JavascriptParser.cs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public void Reset()
3939
InSwitch = false;
4040
Strict = false;
4141
AllowIdentifierEscape = false;
42+
MemberAccessContext = MemberAccessContext.Unknown;
4243

4344
Decorators.Clear();
4445
LabelSet.Clear();
@@ -75,6 +76,7 @@ public void ReleaseLargeBuffers()
7576
public bool InClassBody;
7677
public bool Strict;
7778
public bool AllowIdentifierEscape;
79+
public MemberAccessContext MemberAccessContext;
7880

7981
public ArrayList<Decorator> Decorators;
8082

@@ -83,6 +85,13 @@ public void ReleaseLargeBuffers()
8385
public StrongBox<Token>? FirstCoverInitializedNameError;
8486
}
8587

88+
internal enum MemberAccessContext : byte
89+
{
90+
Unknown = 0,
91+
NewExpressionCallee = 1,
92+
Decorator = 2,
93+
}
94+
8695
[StringMatcher("=", "*=", "**=", "/=", "%=", "+=", "-=", "<<=", ">>=", ">>>=", "&=", "^=", "|=", "&&=", "||=", "??=")]
8796
private static partial bool IsAssignmentOperator(string id);
8897

@@ -1645,7 +1654,11 @@ private Expression ParseNewExpression()
16451654
}
16461655
else
16471656
{
1657+
var previousMemberAccessContext = _context.MemberAccessContext;
1658+
_context.MemberAccessContext = MemberAccessContext.NewExpressionCallee;
16481659
var callee = IsolateCoverGrammar(_parseLeftHandSideExpression);
1660+
_context.MemberAccessContext = previousMemberAccessContext;
1661+
16491662
var args = Match("(") ? ParseArguments() : new NodeList<Expression>();
16501663
expr = new NewExpression(callee, args);
16511664
_context.IsAssignmentTarget = false;
@@ -1827,22 +1840,40 @@ private Expression ParseLeftHandSideExpressionAllowCall()
18271840
}
18281841

18291842
var hasOptional = false;
1843+
var hasCall = false;
18301844
while (true)
18311845
{
18321846
var optional = false;
18331847
if (Match("?."))
18341848
{
1849+
if (_context.MemberAccessContext == MemberAccessContext.Decorator)
1850+
{
1851+
ThrowError(Messages.InvalidDecoratorMemberExpression);
1852+
}
1853+
18351854
optional = true;
18361855
hasOptional = true;
18371856
Expect("?.");
18381857
}
18391858

18401859
if (Match("("))
18411860
{
1861+
if (hasCall && _context.MemberAccessContext == MemberAccessContext.Decorator)
1862+
{
1863+
ThrowError(Messages.InvalidDecoratorMemberExpression);
1864+
}
1865+
1866+
hasCall = true;
1867+
18421868
expr = ParseCallExpression(maybeAsync, startMarker, expr, optional);
18431869
}
18441870
else if (Match("["))
18451871
{
1872+
if (_context.MemberAccessContext == MemberAccessContext.Decorator)
1873+
{
1874+
ThrowError(Messages.InvalidDecoratorMemberExpression);
1875+
}
1876+
18461877
_context.IsBindingElement = false;
18471878
_context.IsAssignmentTarget = !optional;
18481879
Expect("[");
@@ -1852,6 +1883,11 @@ private Expression ParseLeftHandSideExpressionAllowCall()
18521883
}
18531884
else if (_lookahead.Type == TokenType.Template && _lookahead.Head)
18541885
{
1886+
if (_context.MemberAccessContext == MemberAccessContext.Decorator)
1887+
{
1888+
ThrowError(Messages.InvalidDecoratorMemberExpression);
1889+
}
1890+
18551891
// Optional template literal is not included in the spec.
18561892
// https://github.com/tc39/proposal-optional-chaining/issues/54
18571893
if (optional)
@@ -1869,6 +1905,11 @@ private Expression ParseLeftHandSideExpressionAllowCall()
18691905
}
18701906
else if (Match(".") || optional)
18711907
{
1908+
if (hasCall && _context.MemberAccessContext == MemberAccessContext.Decorator)
1909+
{
1910+
ThrowError(Messages.InvalidDecoratorMemberExpression);
1911+
}
1912+
18721913
var previousAllowIdentifierEscape = _context.AllowIdentifierEscape;
18731914

18741915
_context.IsBindingElement = false;
@@ -1957,6 +1998,11 @@ private Expression ParseLeftHandSideExpression()
19571998
var optional = false;
19581999
if (Match("?."))
19592000
{
2001+
if (_context.MemberAccessContext == MemberAccessContext.NewExpressionCallee)
2002+
{
2003+
ThrowError(Messages.InvalidOptionalChainFromNewExpression);
2004+
}
2005+
19602006
optional = true;
19612007
hasOptional = true;
19622008
Expect("?.");
@@ -2028,7 +2074,11 @@ private Expression ParseUpdateExpression()
20282074
}
20292075
else
20302076
{
2077+
var previousMemberAccessContext = _context.MemberAccessContext;
2078+
_context.MemberAccessContext = MemberAccessContext.Unknown;
20312079
expr = InheritCoverGrammar(_parseLeftHandSideExpressionAllowCall);
2080+
_context.MemberAccessContext = previousMemberAccessContext;
2081+
20322082
if (!_hasLineTerminator && _lookahead.Type == TokenType.Punctuator && (Match("++") || Match("--")))
20332083
{
20342084
expr = ParsePostfixUnaryExpression(expr, startMarker);
@@ -2120,7 +2170,7 @@ private UnaryExpression ParseBasicUnaryExpression()
21202170
{
21212171
TolerateError(Messages.StrictDelete);
21222172
}
2123-
if (_context.Strict && unaryExpr.Operator == UnaryOperator.Delete && unaryExpr.Argument is MemberExpression m && m.Property is PrivateIdentifier)
2173+
if (_context.Strict && unaryExpr.Operator == UnaryOperator.Delete && unaryExpr.Argument is MemberExpression { Property: PrivateIdentifier })
21242174
{
21252175
TolerateError(Messages.PrivateFieldNoDelete);
21262176
}
@@ -2566,7 +2616,7 @@ private protected Expression ParseAssignmentExpression()
25662616
_context.IsAssignmentTarget = false;
25672617
_context.IsBindingElement = false;
25682618

2569-
var isAsync = expr is ArrowParameterPlaceHolder arrow && arrow.Async;
2619+
var isAsync = expr is ArrowParameterPlaceHolder { Async: true };
25702620
var result = ReinterpretAsCoverFormalsList(expr);
25712621

25722622
if (result != null)
@@ -4678,7 +4728,11 @@ private Decorator ParseDecorator()
46784728
_context.AllowYield = true;
46794729
_context.IsAsync = false;
46804730

4731+
var previousMemberAccessContext = _context.MemberAccessContext;
4732+
_context.MemberAccessContext = MemberAccessContext.Decorator;
46814733
var expression = IsolateCoverGrammar(_parseLeftHandSideExpressionAllowCall);
4734+
_context.MemberAccessContext = previousMemberAccessContext;
4735+
46824736
_context.Strict = previousStrict;
46834737
_context.AllowYield = previousAllowYield;
46844738
_context.IsAsync = previousIsAsync;
@@ -4991,7 +5045,10 @@ private ClassDeclaration ParseClassDeclarationCore(in Marker node, bool identifi
49915045
if (MatchKeyword("extends"))
49925046
{
49935047
NextToken();
5048+
var previousMemberAccessContext = _context.MemberAccessContext;
5049+
_context.MemberAccessContext = MemberAccessContext.Unknown;
49945050
superClass = IsolateCoverGrammar(_parseLeftHandSideExpressionAllowCall);
5051+
_context.MemberAccessContext = previousMemberAccessContext;
49955052
}
49965053

49975054
var classBody = ParseClassBody(hasSuperClass: superClass is not null);
@@ -5033,7 +5090,10 @@ private ClassExpression ParseClassExpression()
50335090
if (MatchKeyword("extends"))
50345091
{
50355092
NextToken();
5093+
var previousMemberAccessContext = _context.MemberAccessContext;
5094+
_context.MemberAccessContext = MemberAccessContext.Unknown;
50365095
superClass = IsolateCoverGrammar(_parseLeftHandSideExpressionAllowCall);
5096+
_context.MemberAccessContext = previousMemberAccessContext;
50375097
}
50385098

50395099
var classBody = ParseClassBody(hasSuperClass: superClass is not null);

src/Esprima/Messages.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace Esprima;
22

33
// Error messages should be identical to V8.
4+
// TODO: Replace the messages marked with "temporary" with the actual V8 messages once they become available (see https://github.com/v8/v8/blob/main/src/common/message-template.h).
45
internal static class Messages
56
{
67
public const string ArgumentsNotAllowedInClassField = "'arguments' is not allowed in class field initializer or static initialization block";
@@ -22,8 +23,7 @@ internal static class Messages
2223
public const string DuplicateConstructor = "A class may only have one constructor";
2324
public const string DuplicateParameter = "Duplicate parameter name not allowed in this context";
2425
public const string DuplicateProtoProperty = "Duplicate __proto__ fields are not allowed in object literals";
25-
// TODO: Replace this with the actual V8 message once it becomes available (see https://github.com/v8/v8/blob/main/src/common/message-template.h).
26-
public const string DuplicateKeyInImportAttributes = "Import attributes has duplicate key '{0}'";
26+
public const string DuplicateKeyInImportAttributes = "Import attributes has duplicate key '{0}'"; // temporary
2727
public const string ForInOfLoopInitializer = "'{0} loop variable declaration may not have an initializer";
2828
public const string GeneratorInLegacyContext = "Generator declarations are not allowed in legacy contexts";
2929
public const string IllegalBreak = "Illegal break statement";
@@ -32,13 +32,14 @@ internal static class Messages
3232
public const string IllegalImportDeclaration = "Unexpected token";
3333
public const string IllegalLanguageModeDirective = "Illegal 'use strict' directive in function with non-simple parameter list";
3434
public const string IllegalReturn = "Illegal return statement";
35+
public const string InvalidDecoratorMemberExpression = "Invalid decorator member expression"; // temporary
3536
public const string InvalidEscapedReservedWord = "Keyword must not contain escaped characters";
36-
public const string NoSemicolonAfterDecorator = "Decorators must not be followed by a semicolon.";
3737
public const string InvalidHexEscapeSequence = "Invalid hexadecimal escape sequence";
3838
public const string InvalidLHSInAssignment = "Invalid left-hand side in assignment";
3939
public const string InvalidLHSInForIn = "Invalid left-hand side in for-in";
4040
public const string InvalidLHSInForLoop = "Invalid left-hand side in for-loop";
4141
public const string InvalidModuleSpecifier = "Unexpected token";
42+
public const string InvalidOptionalChainFromNewExpression = "Invalid optional chain from new expression";
4243
public const string InvalidRegExpFlags = "Invalid regular expression flags";
4344
public const string InvalidTaggedTemplateOnOptionalChain = "Invalid tagged template on optional chain";
4445
public const string InvalidUnicodeEscapeSequence = "Invalid Unicode escape sequence";
@@ -49,6 +50,7 @@ internal static class Messages
4950
public const string NewTargetNotAllowedHere = "new.target expression is not allowed here";
5051
public const string NoAsAfterImportNamespace = "Unexpected token";
5152
public const string NoCatchOrFinally = "Missing catch or finally after try";
53+
public const string NoSemicolonAfterDecorator = "Decorators must not be followed by a semicolon.";
5254
public const string NumericSeparatorAfterLeadingZero = "Numeric separator can not be used after leading 0";
5355
public const string NumericSeparatorNotAllowedHere = "Numeric separator is not allowed here";
5456
public const string NumericSeparatorOneUnderscore = "Numeric separator must be exactly one underscore";

src/Esprima/Utils/AstToJavascriptConverter.Enums.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ protected internal enum ExpressionFlags
3636
SpaceAfterBracketsRecommended = JavaScriptTextWriter.ExpressionFlags.SpaceAfterBracketsRecommended,
3737
SpaceAroundBracketsRecommended = JavaScriptTextWriter.ExpressionFlags.SpaceAroundBracketsRecommended,
3838

39-
IsMethod = 1 << 16,
39+
IsRootExpression = 1 << 16,
40+
41+
IsMethod = 1 << 17,
42+
43+
IsInsideDecorator = 1 << 22, // automatically propagated to sub-expressions
4044

4145
IsInAmbiguousInOperatorContext = 1 << 24, // automatically propagated to sub-expressions
4246

@@ -52,6 +56,6 @@ protected internal enum ExpressionFlags
5256

5357
IsInsideStatementExpression = 1 << 31, // automatically propagated to sub-expressions
5458

55-
IsInPotentiallyAmbiguousContext = IsInAmbiguousInOperatorContext | IsInsideArrowFunctionBody | IsInsideNewCallee | IsInsideLeftHandSideExpression | IsInsideStatementExpression,
59+
IsInPotentiallyAmbiguousContext = IsInAmbiguousInOperatorContext | IsInsideArrowFunctionBody | IsInsideDecorator | IsInsideNewCallee | IsInsideLeftHandSideExpression | IsInsideStatementExpression,
5660
}
5761
}

src/Esprima/Utils/AstToJavascriptConverter.Helpers.cs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,7 @@ protected void VisitStatementListItem(Statement statement, int index, int count,
9494
[MethodImpl(MethodImplOptions.AggressiveInlining)]
9595
private protected static ExpressionFlags RootExpressionFlags(bool needsBrackets)
9696
{
97-
return ExpressionFlags.IsLeftMost | needsBrackets.ToFlag(ExpressionFlags.NeedsBrackets);
98-
}
99-
100-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
101-
private protected static ExpressionFlags LeftHandSideRootExpressionFlags(bool needsBrackets)
102-
{
103-
return ExpressionFlags.IsInsideLeftHandSideExpression | ExpressionFlags.IsLeftMostInLeftHandSideExpression | RootExpressionFlags(needsBrackets);
97+
return ExpressionFlags.IsRootExpression | ExpressionFlags.IsLeftMost | needsBrackets.ToFlag(ExpressionFlags.NeedsBrackets);
10498
}
10599

106100
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -127,7 +121,7 @@ protected ExpressionFlags PropagateExpressionFlags(ExpressionFlags flags)
127121
flags = flags & ~isLeftMostFlags | _currentExpressionFlags & isLeftMostFlags;
128122
}
129123

130-
// Propagates IsInsideStatementExpression, IsInsideArrowFunctionBody and IsInsideLeftHandSideExpression to current expression.
124+
// Propagates IsInAmbiguousInOperatorContext and IsInside* flags to current expression.
131125
flags |= _currentExpressionFlags & ExpressionFlags.IsInPotentiallyAmbiguousContext;
132126

133127
return flags;
@@ -144,9 +138,10 @@ protected ExpressionFlags DisambiguateExpression(Expression expression, Expressi
144138
if ((flags & ExpressionFlags.IsInPotentiallyAmbiguousContext) != 0)
145139
{
146140
if (flags.HasFlagFast(ExpressionFlags.IsInsideStatementExpression | ExpressionFlags.IsLeftMost) && ExpressionIsAmbiguousAsStatementExpression(expression) ||
147-
flags.HasFlagFast(ExpressionFlags.IsInsideLeftHandSideExpression | ExpressionFlags.IsLeftMostInLeftHandSideExpression) && LeftHandSideExpressionIsParenthesized(expression) ||
148141
flags.HasFlagFast(ExpressionFlags.IsInsideArrowFunctionBody | ExpressionFlags.IsLeftMostInArrowFunctionBody) && ExpressionIsAmbiguousAsArrowFunctionBody(expression) ||
149-
flags.HasFlagFast(ExpressionFlags.IsInsideNewCallee | ExpressionFlags.IsLeftMostInNewCallee) && ExpressionIsAmbiguousAsNewCallee(expression))
142+
flags.HasFlagFast(ExpressionFlags.IsInsideNewCallee | ExpressionFlags.IsLeftMostInNewCallee) && ExpressionIsAmbiguousAsNewCallee(expression) ||
143+
flags.HasFlagFast(ExpressionFlags.IsInsideLeftHandSideExpression | ExpressionFlags.IsLeftMostInLeftHandSideExpression) && LeftHandSideExpressionIsParenthesized(expression) ||
144+
flags.HasFlagFast(ExpressionFlags.IsInsideDecorator | ExpressionFlags.IsLeftMost) && DecoratorLeftMostExpressionIsParenthesized(expression, isRoot: flags.HasFlagFast(ExpressionFlags.IsRootExpression)))
150145
{
151146
return (flags | ExpressionFlags.NeedsBrackets) & ~ExpressionFlags.IsInAmbiguousInOperatorContext;
152147
}
@@ -327,6 +322,22 @@ protected virtual bool LeftHandSideExpressionIsParenthesized(Expression expressi
327322
return false;
328323
}
329324

325+
protected virtual bool DecoratorLeftMostExpressionIsParenthesized(Expression expression, bool isRoot)
326+
{
327+
// https://tc39.es/proposal-decorators/
328+
329+
switch (expression.Type)
330+
{
331+
case Nodes.Identifier:
332+
case Nodes.MemberExpression when expression.As<MemberExpression>() is { Computed: false }:
333+
return false;
334+
case Nodes.CallExpression:
335+
return !isRoot;
336+
}
337+
338+
return true;
339+
}
340+
330341
protected virtual bool ExpressionNeedsBracketsInList(Expression expression)
331342
{
332343
return expression.Type is

src/Esprima/Utils/AstToJavascriptConverter.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ binaryExpression.Right is UnaryExpression rightUnaryExpression &&
456456
Writer.WriteKeyword("extends", TokenFlags.SurroundingSpaceRecommended, ref _writeContext);
457457

458458
_writeContext.SetNodeProperty(nameof(classDeclaration.SuperClass), static node => node.As<ClassDeclaration>().SuperClass);
459-
VisitRootExpression(classDeclaration.SuperClass, LeftHandSideRootExpressionFlags(needsBrackets: false));
459+
VisitRootExpression(classDeclaration.SuperClass, ExpressionFlags.IsInsideLeftHandSideExpression | ExpressionFlags.IsLeftMostInLeftHandSideExpression | RootExpressionFlags(needsBrackets: false));
460460
}
461461

462462
_writeContext.SetNodeProperty(nameof(classDeclaration.Body), static node => node.As<ClassDeclaration>().Body);
@@ -489,7 +489,7 @@ binaryExpression.Right is UnaryExpression rightUnaryExpression &&
489489
Writer.WriteKeyword("extends", TokenFlags.SurroundingSpaceRecommended, ref _writeContext);
490490

491491
_writeContext.SetNodeProperty(nameof(classExpression.SuperClass), static node => node.As<ClassExpression>().SuperClass);
492-
VisitRootExpression(classExpression.SuperClass, LeftHandSideRootExpressionFlags(needsBrackets: false));
492+
VisitRootExpression(classExpression.SuperClass, ExpressionFlags.IsInsideLeftHandSideExpression | ExpressionFlags.IsLeftMostInLeftHandSideExpression | RootExpressionFlags(needsBrackets: false));
493493
}
494494

495495
_writeContext.SetNodeProperty(nameof(classExpression.Body), static node => node.As<ClassExpression>().Body);
@@ -558,7 +558,7 @@ binaryExpression.Right is UnaryExpression rightUnaryExpression &&
558558
Writer.WritePunctuator("@", TokenFlags.Leading | (ParentNode is not Expression).ToFlag(TokenFlags.LeadingSpaceRecommended), ref _writeContext);
559559

560560
_writeContext.SetNodeProperty(nameof(decorator.Expression), static node => node.As<Decorator>().Expression);
561-
VisitRootExpression(decorator.Expression, LeftHandSideRootExpressionFlags(needsBrackets: false));
561+
VisitRootExpression(decorator.Expression, ExpressionFlags.IsInsideDecorator | RootExpressionFlags(needsBrackets: false));
562562

563563
Writer.SpaceRecommendedAfterLastToken();
564564

src/Esprima/Utils/JavascriptTextWriter.Enums.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public enum StatementFlags
115115
/// 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).
116116
/// </summary>
117117
/// <remarks>
118-
/// In the the visitation handlers of <see cref="AstToJavaScriptConverter"/> the flag is interpreted differently: it indicates that the statement comes last in the parent statement.
118+
/// In the visitation handlers of <see cref="AstToJavaScriptConverter"/> the flag is interpreted differently: it indicates that the statement comes last in the parent statement.
119119
/// (Upon visiting a statement, this flag of the parent and child statement gets combined to determine its effective value for the current statement list.)
120120
/// </remarks>
121121
IsRightMost = 1 << 2,
@@ -140,7 +140,7 @@ public enum ExpressionFlags
140140
/// 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).
141141
/// </summary>
142142
/// <remarks>
143-
/// In the the visitation handlers of <see cref="AstToJavaScriptConverter"/> the flag is interpreted differently: it indicates that the expression comes first in the parent expression.
143+
/// In the visitation handlers of <see cref="AstToJavaScriptConverter"/> the flag is interpreted differently: it indicates that the expression comes first in the parent expression.
144144
/// (Upon visiting an expression, this flag of the parent and child expression gets combined to determine its effective value for the expression tree.)
145145
/// </remarks>
146146
IsLeftMost = 1 << 1,

0 commit comments

Comments
 (0)