From 2f1b5c8ae1c16c24feb134dce2cb13437ef9ba23 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Fri, 17 Jul 2026 11:57:36 +0200 Subject: [PATCH 1/2] Prefilter methods that cannot return QueryBuilder Inspect declared method variants before argument-dependent selection and return early only when every variant definitely cannot return QueryBuilder. Uncertain, generic, mixed, and compatible variants continue through the existing resolution path. Most method candidates do not return QueryBuilder. This conservative filter avoids ParametersAcceptorSelector and downstream work for definite negatives while preserving fallback behavior whenever the declaration is not conclusive. Benchmark: uncached Sylius analysis of 2,366 files with eight workers, five runs per variant. Median elapsed time fell from 28.91 s to 28.45 s (-1.6%) and aggregate user+sys CPU from 176.22 s to 170.75 s (-3.1%). --- ...ryBuilderExpressionTypeResolverExtension.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php b/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php index 5f308ba1..e4e3a728 100644 --- a/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php +++ b/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php @@ -48,9 +48,22 @@ public function getType(Expr $expr, Scope $scope): ?Type return null; } - $returnType = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants())->getReturnType(); + $queryBuilderType = new ObjectType(QueryBuilder::class); + $variants = $methodReflection->getVariants(); + $canReturnQueryBuilder = false; + foreach ($variants as $variant) { + if (!$queryBuilderType->isSuperTypeOf($variant->getReturnType())->no()) { + $canReturnQueryBuilder = true; + break; + } + } + if (!$canReturnQueryBuilder) { + return null; + } + + $returnType = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $variants)->getReturnType(); - $returnsQueryBuilder = (new ObjectType(QueryBuilder::class))->isSuperTypeOf($returnType)->yes(); + $returnsQueryBuilder = $queryBuilderType->isSuperTypeOf($returnType)->yes(); if (!$returnsQueryBuilder) { return null; From 80fb4911b79aa5b623df64a3b70f42e9a592b7dd Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Thu, 23 Jul 2026 11:03:24 +0200 Subject: [PATCH 2/2] Test nullable QueryBuilder return types --- .../queryBuilderExpressionTypeResolver.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php b/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php index 2a6af1d9..e8a913a8 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php +++ b/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php @@ -66,6 +66,11 @@ public function testFirstClassCallableDoesNotFail(EntityManagerInterface $em): v $this->getQueryBuilder(...); } + public function testNullableQueryBuilderIsNotInferred(EntityManagerInterface $em): void + { + assertType('Doctrine\\ORM\\QueryBuilder|null', $this->getNullableQueryBuilder($em)); + } + private function adjustQueryBuilderToIndexByInt(QueryBuilder $qb): void { $qb->indexBy('m', 'm.intColumn'); @@ -100,6 +105,13 @@ private static function getStaticQueryBuilder(EntityManagerInterface $em): Query ->from(Many::class, 'm'); } + private function getNullableQueryBuilder(EntityManagerInterface $em): ?QueryBuilder + { + return $em->createQueryBuilder() + ->select('m') + ->from(Many::class, 'm'); + } + private function getBranchingQueryBuilder(EntityManagerInterface $em): QueryBuilder { $queryBuilder = $em->createQueryBuilder()