private function parseBinaryExpressionOrHigher()

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;
    }