in src/Parser.php [2024:2165]
private function parseBinaryExpressionOrHigher($precedence, $parentNode) {
$leftOperand = $this->parseUnaryExpressionOrHigher($parentNode);
[$prevNewPrecedence, $prevAssociativity] = self::UNKNOWN_PRECEDENCE_AND_ASSOCIATIVITY;
while (true) {
$token = $this->getCurrentToken();
[$newPrecedence, $associativity] = $this->getBinaryOperatorPrecedenceAndAssociativity($token);
// Expressions using operators w/o associativity (equality, relational, instanceof)
// cannot reference identical expression types within one of their operands.
//
// Example:
// $a < $b < $c // CASE 1: INVALID
// $a < $b === $c < $d // CASE 2: VALID
//
// In CASE 1, it is expected that we stop parsing the expression after the $b token.
if ($prevAssociativity === Associativity::None && $prevNewPrecedence === $newPrecedence) {
break;
}
// Precedence and associativity properties determine whether we recurse, and continue
// building up the current operand, or whether we pop out.
//
// Example:
// $a + $b + $c // CASE 1: additive-expression (left-associative)
// $a = $b = $c // CASE 2: equality-expression (right-associative)
//
// CASE 1:
// The additive-expression is left-associative, which means we expect the grouping to be:
// ($a + $b) + $c
//
// Because both + operators have the same precedence, and the + operator is left associative,
// we expect the second + operator NOT to be consumed because $newPrecedence > $precedence => FALSE
//
// CASE 2:
// The equality-expression is right-associative, which means we expect the grouping to be:
// $a = ($b = $c)
//
// Because both = operators have the same precedence, and the = operator is right-associative,
// we expect the second = operator to be consumed because $newPrecedence >= $precedence => TRUE
$shouldConsumeCurrentOperator =
$associativity === Associativity::Right ?
$newPrecedence >= $precedence:
$newPrecedence > $precedence;
if (!$shouldConsumeCurrentOperator) {
break;
}
// Unlike every other binary expression, exponentiation operators take precedence over unary operators.
//
// Example:
// -3**2 => -9
//
// In these cases, we strip the UnaryExpression operator, and reassign $leftOperand to
// $unaryExpression->operand.
//
// After we finish building the BinaryExpression, we rebuild the UnaryExpression so that it includes
// the original operator, and the newly constructed exponentiation-expression as the operand.
$shouldOperatorTakePrecedenceOverUnary = false;
switch ($token->kind) {
case TokenKind::AsteriskAsteriskToken:
$shouldOperatorTakePrecedenceOverUnary = $leftOperand instanceof UnaryExpression;
break;
case TokenKind::EqualsToken:
case TokenKind::AsteriskAsteriskEqualsToken:
case TokenKind::AsteriskEqualsToken:
case TokenKind::SlashEqualsToken:
case TokenKind::PercentEqualsToken:
case TokenKind::PlusEqualsToken:
case TokenKind::MinusEqualsToken:
case TokenKind::DotEqualsToken:
case TokenKind::LessThanLessThanEqualsToken:
case TokenKind::GreaterThanGreaterThanEqualsToken:
case TokenKind::AmpersandEqualsToken:
case TokenKind::CaretEqualsToken:
case TokenKind::BarEqualsToken:
case TokenKind::QuestionQuestionEqualsToken:
// Workarounds for https://github.com/Microsoft/tolerant-php-parser/issues/19#issue-201714377
// Parse `!$a = $b` as `!($a = $b)` - PHP constrains the Left Hand Side of an assignment to a variable. A unary operator (`@`, `!`, etc.) is not a variable.
// Instanceof has similar constraints for the LHS.
// So does `!$a += $b`
// TODO: Any other operators?
if ($leftOperand instanceof UnaryOpExpression) {
$shouldOperatorTakePrecedenceOverUnary = true;
}
break;
case TokenKind::InstanceOfKeyword:
// Unlike assignment, the instanceof operator doesn't have restrictions on what can go in the left hand side.
// `!` is the only unary operator with lower precedence than instanceof.
if ($leftOperand instanceof UnaryOpExpression) {
if ($leftOperand->operator->kind === TokenKind::ExclamationToken) {
$shouldOperatorTakePrecedenceOverUnary = true;
}
}
break;
case TokenKind::QuestionToken:
if ($parentNode instanceof TernaryExpression && !isset($parentNode->questionToken)) {
// Workaround to parse "a ? b : c ? d : e" as "(a ? b : c) ? d : e"
break 2;
}
break;
}
if ($shouldOperatorTakePrecedenceOverUnary) {
/** @var UnaryOpExpression $unaryExpression */
$unaryExpression = $leftOperand;
$leftOperand = $unaryExpression->operand;
}
$this->advanceToken();
if ($token->kind === TokenKind::EqualsToken) {
$byRefToken = $this->eatOptional1(TokenKind::AmpersandToken);
}
$leftOperand = $token->kind === TokenKind::QuestionToken ?
$this->parseTernaryExpression($leftOperand, $token, $parentNode) :
$this->makeBinaryExpression(
$leftOperand,
$token,
$byRefToken ?? null,
$this->parseBinaryExpressionOrHigher($newPrecedence, null),
$parentNode);
// Rebuild the unary expression if we deconstructed it earlier.
if ($shouldOperatorTakePrecedenceOverUnary) {
/** @var UnaryOpExpression $unaryExpression */
$leftOperand->parent = $unaryExpression;
$unaryExpression->operand = $leftOperand;
$leftOperand = $unaryExpression;
}
// Hold onto these values, so we know whether we've hit duplicate non-associative operators,
// and need to terminate early.
$prevNewPrecedence = $newPrecedence;
$prevAssociativity = $associativity;
}
return $leftOperand;
}