Skip to content

Commit fefba05

Browse files
committed
support static call
1 parent d698adc commit fefba05

File tree

3 files changed

+58
-51
lines changed

3 files changed

+58
-51
lines changed

src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
namespace PHPStan\Type\Doctrine\QueryBuilder;
44

55
use PhpParser\Node;
6-
use PhpParser\Node\Expr\MethodCall;
7-
use PhpParser\Node\Identifier;
86
use PhpParser\Node\Stmt;
97
use PhpParser\Node\Stmt\Class_;
108
use PhpParser\Node\Stmt\ClassMethod;
@@ -17,13 +15,12 @@
1715
use PHPStan\Analyser\ScopeFactory;
1816
use PHPStan\DependencyInjection\Container;
1917
use PHPStan\Parser\Parser;
20-
use PHPStan\Reflection\ReflectionProvider;
18+
use PHPStan\Reflection\MethodReflection;
2119
use PHPStan\Type\Generic\TemplateTypeMap;
2220
use PHPStan\Type\IntersectionType;
2321
use PHPStan\Type\Type;
2422
use PHPStan\Type\TypeTraverser;
2523
use PHPStan\Type\UnionType;
26-
use function count;
2724
use function is_array;
2825

2926
class OtherMethodQueryBuilderParser
@@ -32,54 +29,28 @@ class OtherMethodQueryBuilderParser
3229
/** @var bool */
3330
private $descendIntoOtherMethods;
3431

35-
/** @var ReflectionProvider */
36-
private $reflectionProvider;
37-
3832
/** @var Parser */
3933
private $parser;
4034

4135
/** @var Container */
4236
private $container;
4337

44-
public function __construct(bool $descendIntoOtherMethods, ReflectionProvider $reflectionProvider, Parser $parser, Container $container)
38+
public function __construct(bool $descendIntoOtherMethods, Parser $parser, Container $container)
4539
{
4640
$this->descendIntoOtherMethods = $descendIntoOtherMethods;
47-
$this->reflectionProvider = $reflectionProvider;
4841
$this->parser = $parser;
4942
$this->container = $container;
5043
}
5144

5245
/**
53-
* @return QueryBuilderType[]
46+
* @return list<QueryBuilderType>
5447
*/
55-
public function findQueryBuilderTypesInCalledMethod(Scope $scope, MethodCall $methodCall): array
48+
public function findQueryBuilderTypesInCalledMethod(Scope $scope, MethodReflection $methodReflection): array
5649
{
5750
if (!$this->descendIntoOtherMethods) {
5851
return [];
5952
}
6053

61-
$methodCalledOnType = $scope->getType($methodCall->var);
62-
if (!$methodCall->name instanceof Identifier) {
63-
return [];
64-
}
65-
66-
$methodCalledOnTypeClassNames = $methodCalledOnType->getObjectClassNames();
67-
68-
if (count($methodCalledOnTypeClassNames) !== 1) {
69-
return [];
70-
}
71-
72-
if (!$this->reflectionProvider->hasClass($methodCalledOnTypeClassNames[0])) {
73-
return [];
74-
}
75-
76-
$classReflection = $this->reflectionProvider->getClass($methodCalledOnTypeClassNames[0]);
77-
$methodName = $methodCall->name->toString();
78-
if (!$classReflection->hasNativeMethod($methodName)) {
79-
return [];
80-
}
81-
82-
$methodReflection = $classReflection->getNativeMethod($methodName);
8354
$fileName = $methodReflection->getDeclaringClass()->getFileName();
8455
if ($fileName === null) {
8556
return [];

src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
use Doctrine\ORM\EntityRepository;
66
use Doctrine\ORM\QueryBuilder;
77
use PhpParser\Node\Expr;
8+
use PhpParser\Node\Expr\CallLike;
89
use PhpParser\Node\Expr\MethodCall;
10+
use PhpParser\Node\Expr\StaticCall;
911
use PhpParser\Node\Identifier;
12+
use PhpParser\Node\Name;
1013
use PHPStan\Analyser\Scope;
14+
use PHPStan\Reflection\MethodReflection;
1115
use PHPStan\Reflection\ParametersAcceptorSelector;
1216
use PHPStan\Type\ExpressionTypeResolverExtension;
1317
use PHPStan\Type\ObjectType;
@@ -30,30 +34,15 @@ public function __construct(
3034

3135
public function getType(Expr $expr, Scope $scope): ?Type
3236
{
33-
if (!$expr instanceof MethodCall) {
37+
if (!$expr instanceof MethodCall && !$expr instanceof StaticCall) {
3438
return null;
3539
}
3640

3741
if ($expr->isFirstClassCallable()) {
3842
return null;
3943
}
4044

41-
if (!$expr->name instanceof Identifier) {
42-
return null;
43-
}
44-
45-
$callerType = $scope->getType($expr->var);
46-
47-
foreach ($callerType->getObjectClassReflections() as $callerClassReflection) {
48-
if ($callerClassReflection->is(QueryBuilder::class)) {
49-
return null; // covered by QueryBuilderMethodDynamicReturnTypeExtension
50-
}
51-
if ($callerClassReflection->is(EntityRepository::class) && $expr->name->name === 'createQueryBuilder') {
52-
return null; // covered by EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension
53-
}
54-
}
55-
56-
$methodReflection = $scope->getMethodReflection($callerType, $expr->name->name);
45+
$methodReflection = $this->getMethodReflection($expr, $scope);
5746

5847
if ($methodReflection === null) {
5948
return null;
@@ -67,12 +56,44 @@ public function getType(Expr $expr, Scope $scope): ?Type
6756
return null;
6857
}
6958

70-
$queryBuilderTypes = $this->otherMethodQueryBuilderParser->findQueryBuilderTypesInCalledMethod($scope, $expr);
59+
$queryBuilderTypes = $this->otherMethodQueryBuilderParser->findQueryBuilderTypesInCalledMethod($scope, $methodReflection);
7160
if (count($queryBuilderTypes) === 0) {
7261
return null;
7362
}
7463

7564
return TypeCombinator::union(...$queryBuilderTypes);
7665
}
7766

67+
/**
68+
* @param StaticCall|MethodCall $call
69+
*/
70+
private function getMethodReflection(CallLike $call, Scope $scope): ?MethodReflection
71+
{
72+
if (!$call->name instanceof Identifier) {
73+
return null;
74+
}
75+
76+
if ($call instanceof MethodCall) {
77+
$callerType = $scope->getType($call->var);
78+
} else {
79+
if (!$call->class instanceof Name) {
80+
return null;
81+
}
82+
$callerType = $scope->resolveTypeByName($call->class);
83+
}
84+
85+
$methodName = $call->name->name;
86+
87+
foreach ($callerType->getObjectClassReflections() as $callerClassReflection) {
88+
if ($callerClassReflection->is(QueryBuilder::class)) {
89+
return null; // covered by QueryBuilderMethodDynamicReturnTypeExtension
90+
}
91+
if ($callerClassReflection->is(EntityRepository::class) && $methodName === 'createQueryBuilder') {
92+
return null; // covered by EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension
93+
}
94+
}
95+
96+
return $scope->getMethodReflection($callerType, $methodName);
97+
}
98+
7899
}

tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ public function testDiveIntoCustomEntityRepository(EntityManagerInterface $em):
5353
assertType('Doctrine\ORM\Query<null, QueryResult\Entities\Many>', $queryBuilder->getQuery());
5454
}
5555

56+
57+
public function testStaticCallWorksToo(EntityManagerInterface $em): void
58+
{
59+
$queryBuilder = self::getStaticQueryBuilder($em);
60+
61+
assertType('Doctrine\ORM\Query<null, QueryResult\Entities\Many>', $queryBuilder->getQuery());
62+
}
63+
5664
public function testFirstClassCallableDoesNotFail(EntityManagerInterface $em): void
5765
{
5866
$this->getQueryBuilder(...);
@@ -70,6 +78,13 @@ private function getQueryBuilder(EntityManagerInterface $em): QueryBuilder
7078
->from(Many::class, 'm');
7179
}
7280

81+
private static function getStaticQueryBuilder(EntityManagerInterface $em): QueryBuilder
82+
{
83+
return $em->createQueryBuilder()
84+
->select('m')
85+
->from(Many::class, 'm');
86+
}
87+
7388
private function getBranchingQueryBuilder(EntityManagerInterface $em): QueryBuilder
7489
{
7590
$queryBuilder = $em->createQueryBuilder()

0 commit comments

Comments
 (0)