Fixes #189 : Fix parsing edge cases in yield statements

Update tests diagnostics and expected generated ASTs

For backwards compatibility, continue to make `yield from expr`
have an ArrayElement as a child node.

To maintain the invariant that all Nodes must have at least one child,
make $yieldExpression->arrayElement be null when parsing `yield;`

Aside: `yield &$a;` is (and should be)
parsed as the binary operator `(yield) & ($a)`.
This is surprising, but makes sense, being the only sane parse tree.

- Add a unit test that `yield & &$a;` is invalid.
  There is no way to parse that.

Verified with the PHP module nikic/php-ast

```php
var_export(ast_dump(
    ast\parse_code('<?php function test() { yield & $x; }', 50)
));
```
This commit is contained in:
Tyson Andre 2018-02-15 19:49:41 -08:00
Родитель faca5af49c
Коммит 816ec34fb6
28 изменённых файлов: 414 добавлений и 108 удалений

Просмотреть файл

@ -226,7 +226,7 @@ public function getEndPosition ( )
> TODO: add doc comment
```php
public static function getTokenKindNameFromValue ( $kindName )
public static function getTokenKindNameFromValue ( $kind )
```
### Token::jsonSerialize
> TODO: add doc comment

Просмотреть файл

@ -2098,7 +2098,23 @@ class Parser {
TokenKind::YieldFromKeyword,
TokenKind::YieldKeyword
);
$yieldExpression->arrayElement = $this->parseArrayElement($yieldExpression);
if ($yieldExpression->yieldOrYieldFromKeyword->kind === TokenKind::YieldFromKeyword) {
// Don't use parseArrayElement. E.g. `yield from &$varName` or `yield from $key => $varName` are both syntax errors
$arrayElement = new ArrayElement();
$arrayElement->parent = $yieldExpression;
$arrayElement->elementValue = $this->parseExpression($arrayElement);
$yieldExpression->arrayElement = $arrayElement;
} else {
// This is always an ArrayElement for backwards compatibilitiy.
// TODO: Can this be changed to a non-ArrayElement in a future release?
if ($this->isExpressionStart($this->getCurrentToken())) {
// Both `yield expr;` and `yield;` are possible.
$yieldExpression->arrayElement = $this->parseArrayElement($yieldExpression);
} else {
$yieldExpression->arrayElement = null;
}
}
return $yieldExpression;
}

Просмотреть файл

@ -1,6 +1,7 @@
<?php
// TODO `return yield` should fail
// `return yield;` is valid code in PHP 7 (invalid in PHP 5),
// since generators can also return values. It is parsed as `return (yield);`
function gen() {
return yield;
}

Просмотреть файл

@ -1,8 +1 @@
[
{
"kind": 0,
"message": "'Expression' expected.",
"start": 75,
"length": 0
}
]
[]

Просмотреть файл

@ -53,18 +53,7 @@
"kind": "YieldKeyword",
"textLength": 5
},
"arrayElement": {
"ArrayElement": {
"elementKey": null,
"arrowToken": null,
"byRef": null,
"elementValue": {
"error": "MissingToken",
"kind": "Expression",
"textLength": 0
}
}
}
"arrayElement": null
}
},
"semicolon": {

Просмотреть файл

@ -1,6 +1,6 @@
<?php
// TODO should fail
// This fails with "'Expression' expected."
function gen() {
return yield from;
}

Просмотреть файл

@ -2,7 +2,7 @@
{
"kind": 0,
"message": "'Expression' expected.",
"start": 65,
"start": 89,
"length": 0
}
]

Просмотреть файл

@ -1,6 +1,7 @@
<?php
// should fail
// should not fail.
// This is parsed as the binary bitwise and operator (yield) & ($a);
function gen() {
yield &$a;
}

Просмотреть файл

@ -44,27 +44,26 @@
{
"ExpressionStatement": {
"expression": {
"YieldExpression": {
"yieldOrYieldFromKeyword": {
"kind": "YieldKeyword",
"textLength": 5
},
"arrayElement": {
"ArrayElement": {
"elementKey": null,
"arrowToken": null,
"byRef": {
"kind": "AmpersandToken",
"textLength": 1
"BinaryExpression": {
"leftOperand": {
"YieldExpression": {
"yieldOrYieldFromKeyword": {
"kind": "YieldKeyword",
"textLength": 5
},
"elementValue": {
"Variable": {
"dollar": null,
"name": {
"kind": "VariableName",
"textLength": 2
}
}
"arrayElement": null
}
},
"operator": {
"kind": "AmpersandToken",
"textLength": 1
},
"rightOperand": {
"Variable": {
"dollar": null,
"name": {
"kind": "VariableName",
"textLength": 2
}
}
}

Просмотреть файл

@ -1 +1,8 @@
[]
[
{
"kind": 0,
"message": "'Expression' expected.",
"start": 53,
"length": 0
}
]

Просмотреть файл

@ -53,16 +53,26 @@
"ArrayElement": {
"elementKey": null,
"arrowToken": null,
"byRef": {
"kind": "AmpersandToken",
"textLength": 1
},
"byRef": null,
"elementValue": {
"Variable": {
"dollar": null,
"name": {
"kind": "VariableName",
"textLength": 2
"BinaryExpression": {
"leftOperand": {
"error": "MissingToken",
"kind": "Expression",
"textLength": 0
},
"operator": {
"kind": "AmpersandToken",
"textLength": 1
},
"rightOperand": {
"Variable": {
"dollar": null,
"name": {
"kind": "VariableName",
"textLength": 2
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,6 @@
<?php
// This is invalid. But (yield) & ($x) would be valid in PHP 7.
function example($x) {
yield & &$x;
}

Просмотреть файл

@ -0,0 +1,8 @@
[
{
"kind": 0,
"message": "'Expression' expected.",
"start": 105,
"length": 0
}
]

Просмотреть файл

@ -0,0 +1,125 @@
{
"SourceFileNode": {
"statementList": [
{
"InlineHtml": {
"scriptSectionEndTag": null,
"text": null,
"scriptSectionStartTag": {
"kind": "ScriptSectionStartTag",
"textLength": 6
}
}
},
{
"FunctionDeclaration": {
"functionKeyword": {
"kind": "FunctionKeyword",
"textLength": 8
},
"byRefToken": null,
"name": {
"kind": "Name",
"textLength": 7
},
"openParen": {
"kind": "OpenParenToken",
"textLength": 1
},
"parameters": {
"ParameterDeclarationList": {
"children": [
{
"Parameter": {
"questionToken": null,
"typeDeclaration": null,
"byRefToken": null,
"dotDotDotToken": null,
"variableName": {
"kind": "VariableName",
"textLength": 2
},
"equalsToken": null,
"default": null
}
}
]
}
},
"closeParen": {
"kind": "CloseParenToken",
"textLength": 1
},
"colonToken": null,
"questionToken": null,
"returnType": null,
"compoundStatementOrSemicolon": {
"CompoundStatementNode": {
"openBrace": {
"kind": "OpenBraceToken",
"textLength": 1
},
"statements": [
{
"ExpressionStatement": {
"expression": {
"BinaryExpression": {
"leftOperand": {
"BinaryExpression": {
"leftOperand": {
"YieldExpression": {
"yieldOrYieldFromKeyword": {
"kind": "YieldKeyword",
"textLength": 5
},
"arrayElement": null
}
},
"operator": {
"kind": "AmpersandToken",
"textLength": 1
},
"rightOperand": {
"error": "MissingToken",
"kind": "Expression",
"textLength": 0
}
}
},
"operator": {
"kind": "AmpersandToken",
"textLength": 1
},
"rightOperand": {
"Variable": {
"dollar": null,
"name": {
"kind": "VariableName",
"textLength": 2
}
}
}
}
},
"semicolon": {
"kind": "SemicolonToken",
"textLength": 1
}
}
}
],
"closeBrace": {
"kind": "CloseBraceToken",
"textLength": 1
}
}
}
}
}
],
"endOfFileToken": {
"kind": "EndOfFileToken",
"textLength": 0
}
}
}

Просмотреть файл

@ -0,0 +1,6 @@
<?php
// This is parsed as (yield) && ($x).
function example($x) {
yield &&$x;
}

Просмотреть файл

@ -0,0 +1 @@
[]

Просмотреть файл

@ -0,0 +1,112 @@
{
"SourceFileNode": {
"statementList": [
{
"InlineHtml": {
"scriptSectionEndTag": null,
"text": null,
"scriptSectionStartTag": {
"kind": "ScriptSectionStartTag",
"textLength": 6
}
}
},
{
"FunctionDeclaration": {
"functionKeyword": {
"kind": "FunctionKeyword",
"textLength": 8
},
"byRefToken": null,
"name": {
"kind": "Name",
"textLength": 7
},
"openParen": {
"kind": "OpenParenToken",
"textLength": 1
},
"parameters": {
"ParameterDeclarationList": {
"children": [
{
"Parameter": {
"questionToken": null,
"typeDeclaration": null,
"byRefToken": null,
"dotDotDotToken": null,
"variableName": {
"kind": "VariableName",
"textLength": 2
},
"equalsToken": null,
"default": null
}
}
]
}
},
"closeParen": {
"kind": "CloseParenToken",
"textLength": 1
},
"colonToken": null,
"questionToken": null,
"returnType": null,
"compoundStatementOrSemicolon": {
"CompoundStatementNode": {
"openBrace": {
"kind": "OpenBraceToken",
"textLength": 1
},
"statements": [
{
"ExpressionStatement": {
"expression": {
"BinaryExpression": {
"leftOperand": {
"YieldExpression": {
"yieldOrYieldFromKeyword": {
"kind": "YieldKeyword",
"textLength": 5
},
"arrayElement": null
}
},
"operator": {
"kind": "AmpersandAmpersandToken",
"textLength": 2
},
"rightOperand": {
"Variable": {
"dollar": null,
"name": {
"kind": "VariableName",
"textLength": 2
}
}
}
}
},
"semicolon": {
"kind": "SemicolonToken",
"textLength": 1
}
}
}
],
"closeBrace": {
"kind": "CloseBraceToken",
"textLength": 1
}
}
}
}
}
],
"endOfFileToken": {
"kind": "EndOfFileToken",
"textLength": 0
}
}
}

Просмотреть файл

@ -1,6 +1,6 @@
<?php
// TODO technically should fail
// Fails with the message "';' expected.", "Unexpected '=>'"
function gen() {
yield from 1 => 2;
}

Просмотреть файл

@ -1 +1,14 @@
[]
[
{
"kind": 0,
"message": "';' expected.",
"start": 101,
"length": 0
},
{
"kind": 0,
"message": "Unexpected '=>'",
"start": 102,
"length": 2
}
]

Просмотреть файл

@ -51,18 +51,8 @@
},
"arrayElement": {
"ArrayElement": {
"elementKey": {
"NumericLiteral": {
"children": {
"kind": "IntegerLiteralToken",
"textLength": 1
}
}
},
"arrowToken": {
"kind": "DoubleArrowToken",
"textLength": 2
},
"elementKey": null,
"arrowToken": null,
"byRef": null,
"elementValue": {
"NumericLiteral": {
@ -76,6 +66,28 @@
}
}
},
"semicolon": {
"error": "MissingToken",
"kind": "SemicolonToken",
"textLength": 0
}
}
},
{
"error": "SkippedToken",
"kind": "DoubleArrowToken",
"textLength": 2
},
{
"ExpressionStatement": {
"expression": {
"NumericLiteral": {
"children": {
"kind": "IntegerLiteralToken",
"textLength": 1
}
}
},
"semicolon": {
"kind": "SemicolonToken",
"textLength": 1

Просмотреть файл

@ -1,6 +1,6 @@
<?php
// TODO technically should fail
// Fails with the messages "';' expected.", "Unexpected '=>'"
function gen() {
yield from $i => $a;
}

Просмотреть файл

@ -1 +1,14 @@
[]
[
{
"kind": 0,
"message": "';' expected.",
"start": 103,
"length": 0
},
{
"kind": 0,
"message": "Unexpected '=>'",
"start": 104,
"length": 2
}
]

Просмотреть файл

@ -51,19 +51,8 @@
},
"arrayElement": {
"ArrayElement": {
"elementKey": {
"Variable": {
"dollar": null,
"name": {
"kind": "VariableName",
"textLength": 2
}
}
},
"arrowToken": {
"kind": "DoubleArrowToken",
"textLength": 2
},
"elementKey": null,
"arrowToken": null,
"byRef": null,
"elementValue": {
"Variable": {
@ -78,6 +67,29 @@
}
}
},
"semicolon": {
"error": "MissingToken",
"kind": "SemicolonToken",
"textLength": 0
}
}
},
{
"error": "SkippedToken",
"kind": "DoubleArrowToken",
"textLength": 2
},
{
"ExpressionStatement": {
"expression": {
"Variable": {
"dollar": null,
"name": {
"kind": "VariableName",
"textLength": 2
}
}
},
"semicolon": {
"kind": "SemicolonToken",
"textLength": 1

Просмотреть файл

@ -1,8 +1 @@
[
{
"kind": 0,
"message": "'Expression' expected.",
"start": 33,
"length": 0
}
]
[]

Просмотреть файл

@ -49,18 +49,7 @@
"kind": "YieldKeyword",
"textLength": 5
},
"arrayElement": {
"ArrayElement": {
"elementKey": null,
"arrowToken": null,
"byRef": null,
"elementValue": {
"error": "MissingToken",
"kind": "Expression",
"textLength": 0
}
}
}
"arrayElement": null
}
},
"semicolon": {

Просмотреть файл

@ -1,6 +1,6 @@
<?php
// TODO should fail
// Fails with the message "'Expression' expected."
function gen() {
yield from;
}

Просмотреть файл

@ -2,7 +2,7 @@
{
"kind": 0,
"message": "'Expression' expected.",
"start": 58,
"start": 89,
"length": 0
}
]

Просмотреть файл

@ -1,6 +1,6 @@
<?php
// TODO `return yield` should fail
// TODO `return yield` should fail in php5
function gen() {
return yield a();
}