diff --git a/src/Esprima/JavaScriptParser.cs b/src/Esprima/JavaScriptParser.cs index d897a870..fde12fbb 100644 --- a/src/Esprima/JavaScriptParser.cs +++ b/src/Esprima/JavaScriptParser.cs @@ -5421,9 +5421,7 @@ private ExportDeclaration ParseExportDeclaration() // export default {}; // export default []; // export default (1 + 2); - var declaration = Match("{") - ? ParseObjectInitializer() - : Match("[") ? ParseArrayInitializer() : ParseAssignmentExpression(); + var declaration = ParseAssignmentExpression(); ConsumeSemicolon(); exportDeclaration = Finalize(node, new ExportDefaultDeclaration(declaration)); diff --git a/src/Esprima/Utils/AstToJavaScriptConverter.Enums.cs b/src/Esprima/Utils/AstToJavaScriptConverter.Enums.cs index e81c8a69..99792cf6 100644 --- a/src/Esprima/Utils/AstToJavaScriptConverter.Enums.cs +++ b/src/Esprima/Utils/AstToJavaScriptConverter.Enums.cs @@ -40,6 +40,8 @@ protected internal enum ExpressionFlags IsMethod = 1 << 17, + IsInsideExportDefaultExpression = 1 << 21, // automatically propagated to sub-expressions + IsInsideDecorator = 1 << 22, // automatically propagated to sub-expressions IsInAmbiguousInOperatorContext = 1 << 24, // automatically propagated to sub-expressions @@ -56,6 +58,6 @@ protected internal enum ExpressionFlags IsInsideStatementExpression = 1 << 31, // automatically propagated to sub-expressions - IsInPotentiallyAmbiguousContext = IsInAmbiguousInOperatorContext | IsInsideArrowFunctionBody | IsInsideDecorator | IsInsideNewCallee | IsInsideLeftHandSideExpression | IsInsideStatementExpression, + IsInPotentiallyAmbiguousContext = IsInAmbiguousInOperatorContext | IsInsideArrowFunctionBody | IsInsideDecorator | IsInsideNewCallee | IsInsideLeftHandSideExpression | IsInsideStatementExpression | IsInsideExportDefaultExpression, } } diff --git a/src/Esprima/Utils/AstToJavaScriptConverter.Helpers.cs b/src/Esprima/Utils/AstToJavaScriptConverter.Helpers.cs index b548cb8e..a545490d 100644 --- a/src/Esprima/Utils/AstToJavaScriptConverter.Helpers.cs +++ b/src/Esprima/Utils/AstToJavaScriptConverter.Helpers.cs @@ -138,16 +138,21 @@ protected ExpressionFlags DisambiguateExpression(Expression expression, Expressi // 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) || + var isAmbiguousExpression = flags.HasFlag(ExpressionFlags.IsLeftMost) && + (flags.HasFlagFast(ExpressionFlags.IsInsideStatementExpression) && ExpressionIsAmbiguousAsStatementExpression(expression) || + flags.HasFlagFast(ExpressionFlags.IsInsideExportDefaultExpression) && ExpressionIsAmbiguousAsExportDefaultExpression(expression) || + flags.HasFlagFast(ExpressionFlags.IsInsideDecorator) && DecoratorLeftMostExpressionIsParenthesized(expression, isRoot: flags.HasFlagFast(ExpressionFlags.IsRootExpression))); + + isAmbiguousExpression = isAmbiguousExpression || flags.HasFlagFast(ExpressionFlags.IsInsideArrowFunctionBody | ExpressionFlags.IsLeftMostInArrowFunctionBody) && ExpressionIsAmbiguousAsArrowFunctionBody(expression) || flags.HasFlagFast(ExpressionFlags.IsInsideNewCallee | ExpressionFlags.IsLeftMostInNewCallee) && ExpressionIsAmbiguousAsNewCallee(expression) || - flags.HasFlagFast(ExpressionFlags.IsInsideLeftHandSideExpression | ExpressionFlags.IsLeftMostInLeftHandSideExpression) && LeftHandSideExpressionIsParenthesized(expression) || - flags.HasFlagFast(ExpressionFlags.IsInsideDecorator | ExpressionFlags.IsLeftMost) && DecoratorLeftMostExpressionIsParenthesized(expression, isRoot: flags.HasFlagFast(ExpressionFlags.IsRootExpression))) - { - return (flags | ExpressionFlags.NeedsBrackets) & ~ExpressionFlags.IsInAmbiguousInOperatorContext; - } + flags.HasFlagFast(ExpressionFlags.IsInsideLeftHandSideExpression | ExpressionFlags.IsLeftMostInLeftHandSideExpression) && LeftHandSideExpressionIsParenthesized(expression); + // Edge case: for (var a = b = (c in d in e) in x); - else if (flags.HasFlagFast(ExpressionFlags.IsInAmbiguousInOperatorContext) && expression is BinaryExpression { Operator: BinaryOperator.In }) + isAmbiguousExpression = isAmbiguousExpression || + flags.HasFlagFast(ExpressionFlags.IsInAmbiguousInOperatorContext) && expression is BinaryExpression { Operator: BinaryOperator.In }; + + if (isAmbiguousExpression) { return (flags | ExpressionFlags.NeedsBrackets) & ~ExpressionFlags.IsInAmbiguousInOperatorContext; } @@ -276,6 +281,20 @@ protected virtual bool ExpressionIsAmbiguousAsStatementExpression(Expression exp return false; } + protected virtual bool ExpressionIsAmbiguousAsExportDefaultExpression(Expression expression) + { + switch (expression.Type) + { + case Nodes.ClassExpression: + case Nodes.FunctionExpression: + case Nodes.Identifier when Scanner.IsStrictModeReservedWord(expression.As().Name): + case Nodes.SequenceExpression: + return true; + } + + return false; + } + protected virtual bool ExpressionIsAmbiguousAsArrowFunctionBody(Expression expression) { switch (expression.Type) diff --git a/src/Esprima/Utils/AstToJavaScriptConverter.cs b/src/Esprima/Utils/AstToJavaScriptConverter.cs index 7a236a03..e40a6c13 100644 --- a/src/Esprima/Utils/AstToJavaScriptConverter.cs +++ b/src/Esprima/Utils/AstToJavaScriptConverter.cs @@ -290,7 +290,7 @@ public void Convert(Node node) Writer.WritePunctuator("=", TokenFlags.InBetween | TokenFlags.SurroundingSpaceRecommended, ref _writeContext); _writeContext.SetNodeProperty(nameof(assignmentPattern.Right), static node => node.As().Right); - VisitRootExpression(assignmentPattern.Right, RootExpressionFlags(needsBrackets: false)); + VisitRootExpression(assignmentPattern.Right, RootExpressionFlags(needsBrackets: ExpressionNeedsBracketsInList(assignmentPattern.Right))); return assignmentPattern; } @@ -640,7 +640,7 @@ public void Convert(Node node) } else { - VisitRootExpression(exportDefaultDeclaration.Declaration.As(), ExpressionFlags.IsInsideStatementExpression | RootExpressionFlags(needsBrackets: false)); + VisitRootExpression(exportDefaultDeclaration.Declaration.As(), ExpressionFlags.IsInsideExportDefaultExpression | RootExpressionFlags(needsBrackets: false)); StatementNeedsSemicolon(); } diff --git a/test/Esprima.Tests/AstToJavascriptTests.cs b/test/Esprima.Tests/AstToJavascriptTests.cs index bf4113d6..378f974f 100644 --- a/test/Esprima.Tests/AstToJavascriptTests.cs +++ b/test/Esprima.Tests/AstToJavascriptTests.cs @@ -788,6 +788,27 @@ public void ToJavaScriptTest_NullishCoalescingMixedWithLogicalAndOr_ShouldBePare } } + [Theory] + [InlineData("[a = b, c] = [];\n", false)] + [InlineData("[a = (b, c)] = [];\n", false)] + [InlineData("export default a, b;\n", true)] + [InlineData("export default (a, b);\n", false)] + public void ToJavaScriptTest_AmbiguousSequenceExpression_ShouldBeParenthesized(string source, bool expectParseError) + { + source = source.Replace("\n", Environment.NewLine); + var parser = new JavaScriptParser(); + if (!expectParseError) + { + var program = parser.ParseModule(source); + var code = AstToJavaScript.ToJavaScriptString(program, format: true); + Assert.Equal(source, code); + } + else + { + Assert.Throws(() => parser.ParseExpression(source)); + } + } + [Theory] [InlineData(true, @"<>AAA < {} > BBB member member DDD ", diff --git a/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-array-expression.js b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-array-expression.js new file mode 100644 index 00000000..75c61d56 --- /dev/null +++ b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-array-expression.js @@ -0,0 +1,5 @@ +export default [ +].map((e, i) => ({ + ...e, + index: i, +})) \ No newline at end of file diff --git a/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-array-expression.tree.json b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-array-expression.tree.json new file mode 100644 index 00000000..67e47b57 --- /dev/null +++ b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-array-expression.tree.json @@ -0,0 +1,283 @@ +{ + "type": "Program", + "body": [ + { + "type": "ExportDefaultDeclaration", + "declaration": { + "type": "CallExpression", + "callee": { + "type": "MemberExpression", + "computed": false, + "object": { + "type": "ArrayExpression", + "elements": [], + "range": [ + 15, + 18 + ], + "loc": { + "start": { + "line": 1, + "column": 15 + }, + "end": { + "line": 2, + "column": 1 + } + } + }, + "property": { + "type": "Identifier", + "name": "map", + "range": [ + 19, + 22 + ], + "loc": { + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 5 + } + } + }, + "optional": false, + "range": [ + 15, + 22 + ], + "loc": { + "start": { + "line": 1, + "column": 15 + }, + "end": { + "line": 2, + "column": 5 + } + } + }, + "arguments": [ + { + "type": "ArrowFunctionExpression", + "id": null, + "params": [ + { + "type": "Identifier", + "name": "e", + "range": [ + 24, + 25 + ], + "loc": { + "start": { + "line": 2, + "column": 7 + }, + "end": { + "line": 2, + "column": 8 + } + } + }, + { + "type": "Identifier", + "name": "i", + "range": [ + 27, + 28 + ], + "loc": { + "start": { + "line": 2, + "column": 10 + }, + "end": { + "line": 2, + "column": 11 + } + } + } + ], + "body": { + "type": "ObjectExpression", + "properties": [ + { + "type": "SpreadElement", + "argument": { + "type": "Identifier", + "name": "e", + "range": [ + 43, + 44 + ], + "loc": { + "start": { + "line": 3, + "column": 7 + }, + "end": { + "line": 3, + "column": 8 + } + } + }, + "range": [ + 40, + 44 + ], + "loc": { + "start": { + "line": 3, + "column": 4 + }, + "end": { + "line": 3, + "column": 8 + } + } + }, + { + "type": "Property", + "key": { + "type": "Identifier", + "name": "index", + "range": [ + 50, + 55 + ], + "loc": { + "start": { + "line": 4, + "column": 4 + }, + "end": { + "line": 4, + "column": 9 + } + } + }, + "computed": false, + "value": { + "type": "Identifier", + "name": "i", + "range": [ + 57, + 58 + ], + "loc": { + "start": { + "line": 4, + "column": 11 + }, + "end": { + "line": 4, + "column": 12 + } + } + }, + "kind": "init", + "method": false, + "shorthand": false, + "range": [ + 50, + 58 + ], + "loc": { + "start": { + "line": 4, + "column": 4 + }, + "end": { + "line": 4, + "column": 12 + } + } + } + ], + "range": [ + 34, + 61 + ], + "loc": { + "start": { + "line": 2, + "column": 17 + }, + "end": { + "line": 5, + "column": 1 + } + } + }, + "generator": false, + "expression": true, + "strict": true, + "async": false, + "range": [ + 23, + 62 + ], + "loc": { + "start": { + "line": 2, + "column": 6 + }, + "end": { + "line": 5, + "column": 2 + } + } + } + ], + "optional": false, + "range": [ + 15, + 63 + ], + "loc": { + "start": { + "line": 1, + "column": 15 + }, + "end": { + "line": 5, + "column": 3 + } + } + }, + "range": [ + 0, + 63 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 5, + "column": 3 + } + } + } + ], + "sourceType": "module", + "range": [ + 0, + 63 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 5, + "column": 3 + } + } +} \ No newline at end of file diff --git a/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-invalid-sequence-expression.failure.json b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-invalid-sequence-expression.failure.json new file mode 100644 index 00000000..7bd13204 --- /dev/null +++ b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-invalid-sequence-expression.failure.json @@ -0,0 +1 @@ +{"index":34,"lineNumber":1,"column":35,"message":"Line 1: Unexpected token ,","description":"Unexpected token ,"} \ No newline at end of file diff --git a/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-invalid-sequence-expression.js b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-invalid-sequence-expression.js new file mode 100644 index 00000000..5431996e --- /dev/null +++ b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-invalid-sequence-expression.js @@ -0,0 +1 @@ +export default window.x = { p: 1 }, window.x.p \ No newline at end of file diff --git a/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-object-expression.js b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-object-expression.js new file mode 100644 index 00000000..8b5e6bd6 --- /dev/null +++ b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-object-expression.js @@ -0,0 +1,3 @@ +export default { + x: 1 +}.propertyIsEnumerable("x") \ No newline at end of file diff --git a/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-object-expression.tree.json b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-object-expression.tree.json new file mode 100644 index 00000000..4a7542e5 --- /dev/null +++ b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-object-expression.tree.json @@ -0,0 +1,190 @@ +{ + "type": "Program", + "body": [ + { + "type": "ExportDefaultDeclaration", + "declaration": { + "type": "CallExpression", + "callee": { + "type": "MemberExpression", + "computed": false, + "object": { + "type": "ObjectExpression", + "properties": [ + { + "type": "Property", + "key": { + "type": "Identifier", + "name": "x", + "range": [ + 19, + 20 + ], + "loc": { + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 3 + } + } + }, + "computed": false, + "value": { + "type": "Literal", + "value": 1, + "raw": "1", + "range": [ + 22, + 23 + ], + "loc": { + "start": { + "line": 2, + "column": 5 + }, + "end": { + "line": 2, + "column": 6 + } + } + }, + "kind": "init", + "method": false, + "shorthand": false, + "range": [ + 19, + 23 + ], + "loc": { + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 6 + } + } + } + ], + "range": [ + 15, + 25 + ], + "loc": { + "start": { + "line": 1, + "column": 15 + }, + "end": { + "line": 3, + "column": 1 + } + } + }, + "property": { + "type": "Identifier", + "name": "propertyIsEnumerable", + "range": [ + 26, + 46 + ], + "loc": { + "start": { + "line": 3, + "column": 2 + }, + "end": { + "line": 3, + "column": 22 + } + } + }, + "optional": false, + "range": [ + 15, + 46 + ], + "loc": { + "start": { + "line": 1, + "column": 15 + }, + "end": { + "line": 3, + "column": 22 + } + } + }, + "arguments": [ + { + "type": "Literal", + "value": "x", + "raw": "\"x\"", + "range": [ + 47, + 50 + ], + "loc": { + "start": { + "line": 3, + "column": 23 + }, + "end": { + "line": 3, + "column": 26 + } + } + } + ], + "optional": false, + "range": [ + 15, + 51 + ], + "loc": { + "start": { + "line": 1, + "column": 15 + }, + "end": { + "line": 3, + "column": 27 + } + } + }, + "range": [ + 0, + 51 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 27 + } + } + } + ], + "sourceType": "module", + "range": [ + 0, + 51 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 27 + } + } +} \ No newline at end of file diff --git a/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-sequence-expression.js b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-sequence-expression.js new file mode 100644 index 00000000..e5862941 --- /dev/null +++ b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-sequence-expression.js @@ -0,0 +1 @@ +export default (window.x = { p: 1 }, window.x.p) \ No newline at end of file diff --git a/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-sequence-expression.tree.json b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-sequence-expression.tree.json new file mode 100644 index 00000000..41fda61b --- /dev/null +++ b/test/Esprima.Tests/Fixtures/ES6/export-declaration/export-default-sequence-expression.tree.json @@ -0,0 +1,298 @@ +{ + "type": "Program", + "body": [ + { + "type": "ExportDefaultDeclaration", + "declaration": { + "type": "SequenceExpression", + "expressions": [ + { + "type": "AssignmentExpression", + "operator": "=", + "left": { + "type": "MemberExpression", + "computed": false, + "object": { + "type": "Identifier", + "name": "window", + "range": [ + 16, + 22 + ], + "loc": { + "start": { + "line": 1, + "column": 16 + }, + "end": { + "line": 1, + "column": 22 + } + } + }, + "property": { + "type": "Identifier", + "name": "x", + "range": [ + 23, + 24 + ], + "loc": { + "start": { + "line": 1, + "column": 23 + }, + "end": { + "line": 1, + "column": 24 + } + } + }, + "optional": false, + "range": [ + 16, + 24 + ], + "loc": { + "start": { + "line": 1, + "column": 16 + }, + "end": { + "line": 1, + "column": 24 + } + } + }, + "right": { + "type": "ObjectExpression", + "properties": [ + { + "type": "Property", + "key": { + "type": "Identifier", + "name": "p", + "range": [ + 29, + 30 + ], + "loc": { + "start": { + "line": 1, + "column": 29 + }, + "end": { + "line": 1, + "column": 30 + } + } + }, + "computed": false, + "value": { + "type": "Literal", + "value": 1, + "raw": "1", + "range": [ + 32, + 33 + ], + "loc": { + "start": { + "line": 1, + "column": 32 + }, + "end": { + "line": 1, + "column": 33 + } + } + }, + "kind": "init", + "method": false, + "shorthand": false, + "range": [ + 29, + 33 + ], + "loc": { + "start": { + "line": 1, + "column": 29 + }, + "end": { + "line": 1, + "column": 33 + } + } + } + ], + "range": [ + 27, + 35 + ], + "loc": { + "start": { + "line": 1, + "column": 27 + }, + "end": { + "line": 1, + "column": 35 + } + } + }, + "range": [ + 16, + 35 + ], + "loc": { + "start": { + "line": 1, + "column": 16 + }, + "end": { + "line": 1, + "column": 35 + } + } + }, + { + "type": "MemberExpression", + "computed": false, + "object": { + "type": "MemberExpression", + "computed": false, + "object": { + "type": "Identifier", + "name": "window", + "range": [ + 37, + 43 + ], + "loc": { + "start": { + "line": 1, + "column": 37 + }, + "end": { + "line": 1, + "column": 43 + } + } + }, + "property": { + "type": "Identifier", + "name": "x", + "range": [ + 44, + 45 + ], + "loc": { + "start": { + "line": 1, + "column": 44 + }, + "end": { + "line": 1, + "column": 45 + } + } + }, + "optional": false, + "range": [ + 37, + 45 + ], + "loc": { + "start": { + "line": 1, + "column": 37 + }, + "end": { + "line": 1, + "column": 45 + } + } + }, + "property": { + "type": "Identifier", + "name": "p", + "range": [ + 46, + 47 + ], + "loc": { + "start": { + "line": 1, + "column": 46 + }, + "end": { + "line": 1, + "column": 47 + } + } + }, + "optional": false, + "range": [ + 37, + 47 + ], + "loc": { + "start": { + "line": 1, + "column": 37 + }, + "end": { + "line": 1, + "column": 47 + } + } + } + ], + "range": [ + 16, + 47 + ], + "loc": { + "start": { + "line": 1, + "column": 16 + }, + "end": { + "line": 1, + "column": 47 + } + } + }, + "range": [ + 0, + 48 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 48 + } + } + } + ], + "sourceType": "module", + "range": [ + 0, + 48 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 48 + } + } +} \ No newline at end of file