diff --git a/src/Parser.php b/src/Parser.php index a82a93b..bb96a1f 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -1844,13 +1844,13 @@ class Parser { TokenKind::LessThanGreaterThanToken => [17, Associativity::None], TokenKind::EqualsEqualsEqualsToken => [17, Associativity::None], TokenKind::ExclamationEqualsEqualsToken => [17, Associativity::None], + TokenKind::LessThanEqualsGreaterThanToken => [17, Associativity::None], // relational-expression (X) TokenKind::LessThanToken => [18, Associativity::None], TokenKind::GreaterThanToken => [18, Associativity::None], TokenKind::LessThanEqualsToken => [18, Associativity::None], TokenKind::GreaterThanEqualsToken => [18, Associativity::None], - TokenKind::LessThanEqualsGreaterThanToken => [18, Associativity::None], // shift-expression (L) TokenKind::LessThanLessThanToken => [19, Associativity::Left], @@ -1883,8 +1883,33 @@ class Parser { return self::UNKNOWN_PRECEDENCE_AND_ASSOCIATIVITY; } + /** + * @internal Do not use outside this class, this may be changed or removed. + */ + const KNOWN_ASSIGNMENT_TOKEN_SET = [ + TokenKind::AsteriskAsteriskEqualsToken => true, + TokenKind::AsteriskEqualsToken => true, + TokenKind::SlashEqualsToken => true, + TokenKind::PercentEqualsToken => true, + TokenKind::PlusEqualsToken => true, + TokenKind::MinusEqualsToken => true, + TokenKind::DotEqualsToken => true, + TokenKind::LessThanLessThanEqualsToken => true, + TokenKind::GreaterThanGreaterThanEqualsToken => true, + TokenKind::AmpersandEqualsToken => true, + TokenKind::CaretEqualsToken => true, + TokenKind::BarEqualsToken => true, + // InstanceOf has other remaining issues, but this heuristic is an improvement for many common cases such as `$x && $y = $z` + ]; + private function makeBinaryExpression($leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode) { $assignmentExpression = $operatorToken->kind === TokenKind::EqualsToken; + if ($assignmentExpression || \array_key_exists($operatorToken->kind, self::KNOWN_ASSIGNMENT_TOKEN_SET)) { + if ($leftOperand instanceof BinaryExpression && !\array_key_exists($leftOperand->operator->kind, self::KNOWN_ASSIGNMENT_TOKEN_SET)) { + // Handle cases without parenthesis, such as $x ** $y === $z, as $x ** ($y === $z) + return $this->shiftBinaryOperands($leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode); + } + } $binaryExpression = $assignmentExpression ? new AssignmentExpression() : new BinaryExpression(); $binaryExpression->parent = $parentNode; $leftOperand->parent = $binaryExpression; @@ -1898,6 +1923,25 @@ class Parser { return $binaryExpression; } + private function shiftBinaryOperands(BinaryExpression $leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode) { + $inner = $this->makeBinaryExpression( + $leftOperand->rightOperand, + $operatorToken, + $byRefToken, + $rightOperand, + $parentNode + ); + $outer = $this->makeBinaryExpression( + $leftOperand->leftOperand, + $leftOperand->operator, + null, + $inner, + $parentNode + ); + $inner->parent = $outer; + return $outer; + } + private function parseDoStatement($parentNode) { $doStatement = new DoStatement(); $doStatement->parent = $parentNode; diff --git a/tests/cases/parser/binaryAssignmentExpressions1.php b/tests/cases/parser/binaryAssignmentExpressions1.php new file mode 100644 index 0000000..00208a9 --- /dev/null +++ b/tests/cases/parser/binaryAssignmentExpressions1.php @@ -0,0 +1,2 @@ + $b; diff --git a/tests/cases/parser/spaceshipExpression1.php.diag b/tests/cases/parser/spaceshipExpression1.php.diag new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/cases/parser/spaceshipExpression1.php.diag @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/cases/parser/spaceshipExpression1.php.tree b/tests/cases/parser/spaceshipExpression1.php.tree new file mode 100644 index 0000000..360ba95 --- /dev/null +++ b/tests/cases/parser/spaceshipExpression1.php.tree @@ -0,0 +1,71 @@ +{ + "SourceFileNode": { + "statementList": [ + { + "InlineHtml": { + "scriptSectionEndTag": null, + "text": null, + "scriptSectionStartTag": { + "kind": "ScriptSectionStartTag", + "textLength": 6 + } + } + }, + { + "ExpressionStatement": { + "expression": { + "BinaryExpression": { + "leftOperand": { + "BinaryExpression": { + "leftOperand": { + "Variable": { + "dollar": null, + "name": { + "kind": "VariableName", + "textLength": 2 + } + } + }, + "operator": { + "kind": "LessThanToken", + "textLength": 1 + }, + "rightOperand": { + "Variable": { + "dollar": null, + "name": { + "kind": "VariableName", + "textLength": 2 + } + } + } + } + }, + "operator": { + "kind": "LessThanEqualsGreaterThanToken", + "textLength": 3 + }, + "rightOperand": { + "Variable": { + "dollar": null, + "name": { + "kind": "VariableName", + "textLength": 2 + } + } + } + } + }, + "semicolon": { + "kind": "SemicolonToken", + "textLength": 1 + } + } + } + ], + "endOfFileToken": { + "kind": "EndOfFileToken", + "textLength": 0 + } + } +} \ No newline at end of file diff --git a/tests/cases/parser/spaceshipExpression2.php b/tests/cases/parser/spaceshipExpression2.php new file mode 100644 index 0000000..101fa3e --- /dev/null +++ b/tests/cases/parser/spaceshipExpression2.php @@ -0,0 +1,3 @@ + $z; diff --git a/tests/cases/parser/spaceshipExpression2.php.diag b/tests/cases/parser/spaceshipExpression2.php.diag new file mode 100644 index 0000000..7b543ed --- /dev/null +++ b/tests/cases/parser/spaceshipExpression2.php.diag @@ -0,0 +1,14 @@ +[ + { + "kind": 0, + "message": "';' expected.", + "start": 40, + "length": 0 + }, + { + "kind": 0, + "message": "Unexpected '<=>'", + "start": 41, + "length": 3 + } +] \ No newline at end of file diff --git a/tests/cases/parser/spaceshipExpression2.php.tree b/tests/cases/parser/spaceshipExpression2.php.tree new file mode 100644 index 0000000..87d6af3 --- /dev/null +++ b/tests/cases/parser/spaceshipExpression2.php.tree @@ -0,0 +1,77 @@ +{ + "SourceFileNode": { + "statementList": [ + { + "InlineHtml": { + "scriptSectionEndTag": null, + "text": null, + "scriptSectionStartTag": { + "kind": "ScriptSectionStartTag", + "textLength": 6 + } + } + }, + { + "ExpressionStatement": { + "expression": { + "BinaryExpression": { + "leftOperand": { + "Variable": { + "dollar": null, + "name": { + "kind": "VariableName", + "textLength": 2 + } + } + }, + "operator": { + "kind": "EqualsEqualsToken", + "textLength": 2 + }, + "rightOperand": { + "Variable": { + "dollar": null, + "name": { + "kind": "VariableName", + "textLength": 2 + } + } + } + } + }, + "semicolon": { + "error": "MissingToken", + "kind": "SemicolonToken", + "textLength": 0 + } + } + }, + { + "error": "SkippedToken", + "kind": "LessThanEqualsGreaterThanToken", + "textLength": 3 + }, + { + "ExpressionStatement": { + "expression": { + "Variable": { + "dollar": null, + "name": { + "kind": "VariableName", + "textLength": 2 + } + } + }, + "semicolon": { + "kind": "SemicolonToken", + "textLength": 1 + } + } + } + ], + "endOfFileToken": { + "kind": "EndOfFileToken", + "textLength": 0 + } + } +} \ No newline at end of file