🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

O(n²) scope merging in BooleanOrHandler::processExpr for deep || chains with instanceof #14477

Description

@ondrejmirtes

Bug report

Deep || chains with instanceof checks cause O(n²) analysis time due to recursive scope merging in BooleanOrHandler::processExpr.

Unlike TypeSpecifier::specifyTypesInCondition which has a specifyTypesForFlattenedBooleanOr optimization for deep chains, BooleanOrHandler::processExpr always processes recursively. At each level, the left scope is merged with the right scope ($leftResult->getScope()->mergeWith($rightResult->getScope())). The merged scope's type for the variable being narrowed grows by one ObjectType per level. The subsequent TypeCombinator::union() inside scope merging must check isSuperTypeOf against all existing union members — O(k) per merge, O(n²) total.

With BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH raised from 4 to 128:

  • 100 instanceof checks: 19s
  • 200 instanceof checks: 186s

Investigation

The bottleneck is in BooleanOrHandler::processExpr (phpstan-src src/Analyser/ExprHandler/BooleanOrHandler.php, line 74-81):

$leftResult = $nodeScopeResolver->processExprNode($stmt, $expr->left, $scope, ...);
$leftFalseyScope = $leftResult->getFalseyScope();
$rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $leftFalseyScope, ...);
$leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope());

This method has no depth check (unlike resolveType at line 51). For n || terms, the recursive processExprNode on the left creates n levels of scope merging. Each merge adds one ObjectType to the union, and TypeCombinator::union(UnionType_k, ObjectType) does O(k) work in the non-scalar comparison loop.

A fix would require a flattened processExpr path for deep OR chains — similar to specifyTypesForFlattenedBooleanOr but for the full expression processing pipeline (including rule invocation, throw points, etc.). This is architecturally non-trivial since each arm currently depends on the previous arm's falsey scope.

Code snippet that reproduces the problem

<?php declare(strict_types = 1);

// Requires BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH raised to 128 to expose.
// At the default limit of 4, the depth check in resolveType masks the issue.

class C1 {} class C2 {} class C3 {} class C4 {} class C5 {}
class C6 {} class C7 {} class C8 {} class C9 {} class C10 {}
class C11 {} class C12 {} class C13 {} class C14 {} class C15 {}
class C16 {} class C17 {} class C18 {} class C19 {} class C20 {}
class C21 {} class C22 {} class C23 {} class C24 {} class C25 {}
class C26 {} class C27 {} class C28 {} class C29 {} class C30 {}
class C31 {} class C32 {} class C33 {} class C34 {} class C35 {}
class C36 {} class C37 {} class C38 {} class C39 {} class C40 {}
class C41 {} class C42 {} class C43 {} class C44 {} class C45 {}
class C46 {} class C47 {} class C48 {} class C49 {} class C50 {}

function test(object $x): void {
    if (
        $x instanceof C1 || $x instanceof C2 || $x instanceof C3 || $x instanceof C4 || $x instanceof C5 ||
        $x instanceof C6 || $x instanceof C7 || $x instanceof C8 || $x instanceof C9 || $x instanceof C10 ||
        $x instanceof C11 || $x instanceof C12 || $x instanceof C13 || $x instanceof C14 || $x instanceof C15 ||
        $x instanceof C16 || $x instanceof C17 || $x instanceof C18 || $x instanceof C19 || $x instanceof C20 ||
        $x instanceof C21 || $x instanceof C22 || $x instanceof C23 || $x instanceof C24 || $x instanceof C25 ||
        $x instanceof C26 || $x instanceof C27 || $x instanceof C28 || $x instanceof C29 || $x instanceof C30 ||
        $x instanceof C31 || $x instanceof C32 || $x instanceof C33 || $x instanceof C34 || $x instanceof C35 ||
        $x instanceof C36 || $x instanceof C37 || $x instanceof C38 || $x instanceof C39 || $x instanceof C40 ||
        $x instanceof C41 || $x instanceof C42 || $x instanceof C43 || $x instanceof C44 || $x instanceof C45 ||
        $x instanceof C46 || $x instanceof C47 || $x instanceof C48 || $x instanceof C49 || $x instanceof C50
    ) {
        echo get_class($x);
    }
}

Expected output

Analysis should complete in under 2 seconds. Currently takes ~19s (with BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 128).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions