diff --git a/src/Analyser/ExprHandlerRegistry.php b/src/Analyser/ExprHandlerRegistry.php new file mode 100644 index 0000000000..95d7489771 --- /dev/null +++ b/src/Analyser/ExprHandlerRegistry.php @@ -0,0 +1,59 @@ +|false>> */ + private static array $exprHandlersByClass = []; + + /** + * @return ExprHandler|null + */ + public static function resolve(Expr $expr, Container $container): ?ExprHandler + { + $cacheKey = get_class($expr); + if ($expr instanceof Expr\CallLike) { + $cacheKey .= '|' . $expr->isFirstClassCallable(); + if ($expr instanceof Expr\New_) { + $cacheKey .= '|' . get_class($expr->class); + } + } + + $containerId = spl_object_id($container); + self::$exprHandlersByClass[$containerId] ??= []; + $cached = self::$exprHandlersByClass[$containerId][$cacheKey] ?? null; + if ($cached !== null) { + return $cached === false ? null : $cached; + } + + $matchedHandler = null; + /** @var ExprHandler $exprHandler */ + foreach ($container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) { + if (!$exprHandler->supports($expr)) { + continue; + } + + $matchedHandler = $exprHandler; + break; + } + + self::$exprHandlersByClass[$containerId][$cacheKey] = $matchedHandler ?? false; + + return $matchedHandler; + } + +} diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index bc65d25f41..60c3326ae3 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1059,12 +1059,8 @@ private function resolveType(string $exprString, Expr $node): Type return $this->expressionTypes[$exprString]->getType(); } - /** @var ExprHandler $exprHandler */ - foreach ($this->container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) { - if (!$exprHandler->supports($node)) { - continue; - } - + $exprHandler = ExprHandlerRegistry::resolve($node, $this->container); + if ($exprHandler !== null) { return $exprHandler->resolveType($this, $node); } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index b979b61b7a..cb407c048a 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -175,7 +175,6 @@ use function array_slice; use function array_values; use function count; -use function get_class; use function in_array; use function is_array; use function is_int; @@ -198,19 +197,6 @@ class NodeScopeResolver /** @var array filePath(string) => bool(true) */ private array $analysedFiles = []; - /** - * Memoizes which ExprHandler handles each Expr class so processExprNode() does not - * re-scan every tagged handler on each call. Keyed by Expr class-string; false means - * no handler matched (default handling). - * - * Call-likes (Expr\CallLike) are never memoized: their handlers select on - * isFirstClassCallable(), which the Expr class alone does not determine. Every other - * handler's supports() is a pure instanceof check, so the class fully determines it. - * - * @var array, ExprHandler|false> - */ - private array $exprHandlersByClass = []; - /** @var array */ private array $earlyTerminatingMethodNames; @@ -2805,7 +2791,7 @@ public function processExprNode( $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $storage, $context); - $exprHandler = $this->resolveExprHandler($expr); + $exprHandler = ExprHandlerRegistry::resolve($expr, $this->container); if ($exprHandler !== null) { return $exprHandler->processExpr($this, $stmt, $expr, $scope, $storage, $nodeCallback, $context); } @@ -2826,40 +2812,6 @@ public function processExprNode( ); } - /** - * Resolves the ExprHandler for the given expression, memoizing the result by Expr class. - * - * @return ExprHandler|null - */ - private function resolveExprHandler(Expr $expr): ?ExprHandler - { - // Call-likes are excluded from the cache: their handlers select on - // isFirstClassCallable(), so the Expr class does not uniquely determine the handler. - if (!$expr instanceof Expr\CallLike) { - $cached = $this->exprHandlersByClass[get_class($expr)] ?? null; - if ($cached !== null) { - return $cached === false ? null : $cached; - } - } - - $matchedHandler = null; - /** @var ExprHandler $exprHandler */ - foreach ($this->container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) { - if (!$exprHandler->supports($expr)) { - continue; - } - - $matchedHandler = $exprHandler; - break; - } - - if (!$expr instanceof Expr\CallLike) { - $this->exprHandlersByClass[get_class($expr)] = $matchedHandler ?? false; - } - - return $matchedHandler; - } - /** * @param 'get'|'set' $hookName * @return InternalThrowPoint[] diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 73c56dbf23..3bc9039336 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -44,7 +44,6 @@ use function array_map; use function array_merge; use function count; -use function get_class; use function in_array; use function strtolower; use function substr; @@ -60,20 +59,6 @@ final class TypeSpecifier /** @var StaticMethodTypeSpecifyingExtension[][]|null */ private ?array $staticMethodTypeSpecifyingExtensionsByClass = null; - /** - * Memoizes which ExprHandler handles each Expr class so specifyTypesInCondition() - * does not re-scan every tagged handler (a linear supports() sweep) on each call. - * This matters for deep boolean chains, where specifyTypesInCondition() runs once per arm. - * Keyed by Expr class-string; false means no handler matched (default narrowing). - * - * Call-likes (Expr\CallLike) are never memoized: their handlers select on - * isFirstClassCallable(), which the Expr class alone does not determine. Every other - * handler's supports() is a pure instanceof check, so the class fully determines it. - * - * @var array, ExprHandler|false> - */ - private array $exprHandlersByClass = []; - /** * @param FunctionTypeSpecifyingExtension[] $functionTypeSpecifyingExtensions * @param MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions @@ -104,7 +89,7 @@ public function specifyTypesInCondition( return (new SpecifiedTypes([], []))->setRootExpr($expr); } - $exprHandler = $this->resolveExprHandler($expr); + $exprHandler = ExprHandlerRegistry::resolve($expr, $this->container); if ($exprHandler !== null) { return $exprHandler->specifyTypes($this, $scope, $expr, $context); } @@ -112,40 +97,6 @@ public function specifyTypesInCondition( return $this->specifyDefaultTypes($scope, $expr, $context); } - /** - * Resolves the ExprHandler for the given expression, memoizing the result by Expr class. - * - * @return ExprHandler|null - */ - private function resolveExprHandler(Expr $expr): ?ExprHandler - { - // Call-likes are excluded from the cache: their handlers select on - // isFirstClassCallable(), so the Expr class does not uniquely determine the handler. - if (!$expr instanceof Expr\CallLike) { - $cached = $this->exprHandlersByClass[get_class($expr)] ?? null; - if ($cached !== null) { - return $cached === false ? null : $cached; - } - } - - $matchedHandler = null; - /** @var ExprHandler $exprHandler */ - foreach ($this->container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) { - if (!$exprHandler->supports($expr)) { - continue; - } - - $matchedHandler = $exprHandler; - break; - } - - if (!$expr instanceof Expr\CallLike) { - $this->exprHandlersByClass[get_class($expr)] = $matchedHandler ?? false; - } - - return $matchedHandler; - } - /** @internal */ public function isNormalCountCall(FuncCall $countFuncCall, Type $typeToCount, Scope $scope): TrinaryLogic {