Skip to content

Commit a9d0aaf

Browse files
authored
Infer QueryBuilderType for any method returning QueryBuilder
1 parent 95959dc commit a9d0aaf

13 files changed

+265
-118
lines changed

extension.neon

+5
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ services:
169169
descendIntoOtherMethods: %doctrine.searchOtherMethodsForQueryBuilderBeginning%
170170
parser: @defaultAnalysisParser
171171

172+
-
173+
class: PHPStan\Type\Doctrine\QueryBuilder\ReturnQueryBuilderExpressionTypeResolverExtension
174+
tags:
175+
- phpstan.broker.expressionTypeResolverExtension
176+
172177
-
173178
class: PHPStan\Stubs\Doctrine\StubFilesExtensionLoader
174179
tags:

rules.neon

-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ services:
3535
class: PHPStan\Rules\Doctrine\ORM\QueryBuilderDqlRule
3636
arguments:
3737
reportDynamicQueryBuilders: %doctrine.reportDynamicQueryBuilders%
38-
searchOtherMethodsForQueryBuilderBeginning: %doctrine.searchOtherMethodsForQueryBuilderBeginning%
3938
tags:
4039
- phpstan.rules.rule
4140
-

src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php

+1-20
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use PHPStan\Rules\RuleErrorBuilder;
1313
use PHPStan\Type\Doctrine\DoctrineTypeUtils;
1414
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
15-
use PHPStan\Type\Doctrine\QueryBuilder\OtherMethodQueryBuilderParser;
1615
use PHPStan\Type\ObjectType;
1716
use PHPStan\Type\TypeUtils;
1817
use Throwable;
@@ -32,23 +31,13 @@ class QueryBuilderDqlRule implements Rule
3231
/** @var bool */
3332
private $reportDynamicQueryBuilders;
3433

35-
/** @var OtherMethodQueryBuilderParser */
36-
private $otherMethodQueryBuilderParser;
37-
38-
/** @var bool */
39-
private $searchOtherMethodsForQueryBuilderBeginning;
40-
4134
public function __construct(
4235
ObjectMetadataResolver $objectMetadataResolver,
43-
OtherMethodQueryBuilderParser $otherMethodQueryBuilderParser,
44-
bool $reportDynamicQueryBuilders,
45-
bool $searchOtherMethodsForQueryBuilderBeginning
36+
bool $reportDynamicQueryBuilders
4637
)
4738
{
4839
$this->objectMetadataResolver = $objectMetadataResolver;
49-
$this->otherMethodQueryBuilderParser = $otherMethodQueryBuilderParser;
5040
$this->reportDynamicQueryBuilders = $reportDynamicQueryBuilders;
51-
$this->searchOtherMethodsForQueryBuilderBeginning = $searchOtherMethodsForQueryBuilderBeginning;
5241
}
5342

5443
public function getNodeType(): string
@@ -69,14 +58,6 @@ public function processNode(Node $node, Scope $scope): array
6958
$calledOnType = $scope->getType($node->var);
7059
$queryBuilderTypes = DoctrineTypeUtils::getQueryBuilderTypes($calledOnType);
7160
if (count($queryBuilderTypes) === 0) {
72-
73-
if ($this->searchOtherMethodsForQueryBuilderBeginning) {
74-
$queryBuilderTypes = $this->otherMethodQueryBuilderParser->getQueryBuilderTypes($scope, $node);
75-
if (count($queryBuilderTypes) !== 0) {
76-
return [];
77-
}
78-
}
79-
8061
if (
8162
$this->reportDynamicQueryBuilders
8263
&& (new ObjectType('Doctrine\ORM\QueryBuilder'))->isSuperTypeOf($calledOnType)->yes()

src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php

+5-41
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,61 +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 getQueryBuilderTypes(Scope $scope, MethodCall $methodCall): array
48+
public function findQueryBuilderTypesInCalledMethod(Scope $scope, MethodReflection $methodReflection): array
5649
{
57-
if (!$this->descendIntoOtherMethods || !$methodCall->var instanceof MethodCall) {
58-
return [];
59-
}
60-
61-
return $this->findQueryBuilderTypesInCalledMethod($scope, $methodCall->var);
62-
}
63-
/**
64-
* @return QueryBuilderType[]
65-
*/
66-
private function findQueryBuilderTypesInCalledMethod(Scope $scope, MethodCall $methodCall): array
67-
{
68-
$methodCalledOnType = $scope->getType($methodCall->var);
69-
if (!$methodCall->name instanceof Identifier) {
70-
return [];
71-
}
72-
73-
$methodCalledOnTypeClassNames = $methodCalledOnType->getObjectClassNames();
74-
75-
if (count($methodCalledOnTypeClassNames) !== 1) {
76-
return [];
77-
}
78-
79-
if (!$this->reflectionProvider->hasClass($methodCalledOnTypeClassNames[0])) {
80-
return [];
81-
}
82-
83-
$classReflection = $this->reflectionProvider->getClass($methodCalledOnTypeClassNames[0]);
84-
$methodName = $methodCall->name->toString();
85-
if (!$classReflection->hasNativeMethod($methodName)) {
50+
if (!$this->descendIntoOtherMethods) {
8651
return [];
8752
}
8853

89-
$methodReflection = $classReflection->getNativeMethod($methodName);
9054
$fileName = $methodReflection->getDeclaringClass()->getFileName();
9155
if ($fileName === null) {
9256
return [];

src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php

+2-10
Original file line numberDiff line numberDiff line change
@@ -65,22 +65,17 @@ class QueryBuilderGetQueryDynamicReturnTypeExtension implements DynamicMethodRet
6565
/** @var DescriptorRegistry */
6666
private $descriptorRegistry;
6767

68-
/** @var OtherMethodQueryBuilderParser */
69-
private $otherMethodQueryBuilderParser;
70-
7168
public function __construct(
7269
ObjectMetadataResolver $objectMetadataResolver,
7370
ArgumentsProcessor $argumentsProcessor,
7471
?string $queryBuilderClass,
75-
DescriptorRegistry $descriptorRegistry,
76-
OtherMethodQueryBuilderParser $otherMethodQueryBuilderParser
72+
DescriptorRegistry $descriptorRegistry
7773
)
7874
{
7975
$this->objectMetadataResolver = $objectMetadataResolver;
8076
$this->argumentsProcessor = $argumentsProcessor;
8177
$this->queryBuilderClass = $queryBuilderClass;
8278
$this->descriptorRegistry = $descriptorRegistry;
83-
$this->otherMethodQueryBuilderParser = $otherMethodQueryBuilderParser;
8479
}
8580

8681
public function getClass(): string
@@ -107,10 +102,7 @@ public function getTypeFromMethodCall(
107102
)->getReturnType();
108103
$queryBuilderTypes = DoctrineTypeUtils::getQueryBuilderTypes($calledOnType);
109104
if (count($queryBuilderTypes) === 0) {
110-
$queryBuilderTypes = $this->otherMethodQueryBuilderParser->getQueryBuilderTypes($scope, $methodCall);
111-
if (count($queryBuilderTypes) === 0) {
112-
return $defaultReturnType;
113-
}
105+
return $defaultReturnType;
114106
}
115107

116108
$objectManager = $this->objectMetadataResolver->getObjectManager();

src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php

+2-10
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,11 @@ class QueryBuilderMethodDynamicReturnTypeExtension implements DynamicMethodRetur
2626
/** @var string|null */
2727
private $queryBuilderClass;
2828

29-
/** @var OtherMethodQueryBuilderParser */
30-
private $otherMethodQueryBuilderParser;
31-
3229
public function __construct(
33-
?string $queryBuilderClass,
34-
OtherMethodQueryBuilderParser $otherMethodQueryBuilderParser
30+
?string $queryBuilderClass
3531
)
3632
{
3733
$this->queryBuilderClass = $queryBuilderClass;
38-
$this->otherMethodQueryBuilderParser = $otherMethodQueryBuilderParser;
3934
}
4035

4136
public function getClass(): string
@@ -74,10 +69,7 @@ public function getTypeFromMethodCall(
7469

7570
$queryBuilderTypes = DoctrineTypeUtils::getQueryBuilderTypes($calledOnType);
7671
if (count($queryBuilderTypes) === 0) {
77-
$queryBuilderTypes = $this->otherMethodQueryBuilderParser->getQueryBuilderTypes($scope, $methodCall);
78-
if (count($queryBuilderTypes) === 0) {
79-
return $calledOnType;
80-
}
72+
return $calledOnType;
8173
}
8274

8375
if (count($queryBuilderTypes) > self::MAX_COMBINATIONS) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\QueryBuilder;
4+
5+
use Doctrine\ORM\EntityManagerInterface;
6+
use Doctrine\ORM\EntityRepository;
7+
use Doctrine\ORM\QueryBuilder;
8+
use PhpParser\Node\Expr;
9+
use PhpParser\Node\Expr\CallLike;
10+
use PhpParser\Node\Expr\MethodCall;
11+
use PhpParser\Node\Expr\StaticCall;
12+
use PhpParser\Node\Identifier;
13+
use PhpParser\Node\Name;
14+
use PHPStan\Analyser\Scope;
15+
use PHPStan\Reflection\MethodReflection;
16+
use PHPStan\Reflection\ParametersAcceptorSelector;
17+
use PHPStan\Type\ExpressionTypeResolverExtension;
18+
use PHPStan\Type\ObjectType;
19+
use PHPStan\Type\Type;
20+
use PHPStan\Type\TypeCombinator;
21+
use function count;
22+
23+
class ReturnQueryBuilderExpressionTypeResolverExtension implements ExpressionTypeResolverExtension
24+
{
25+
26+
/** @var OtherMethodQueryBuilderParser */
27+
private $otherMethodQueryBuilderParser;
28+
29+
public function __construct(
30+
OtherMethodQueryBuilderParser $otherMethodQueryBuilderParser
31+
)
32+
{
33+
$this->otherMethodQueryBuilderParser = $otherMethodQueryBuilderParser;
34+
}
35+
36+
public function getType(Expr $expr, Scope $scope): ?Type
37+
{
38+
if (!$expr instanceof MethodCall && !$expr instanceof StaticCall) {
39+
return null;
40+
}
41+
42+
if ($expr->isFirstClassCallable()) {
43+
return null;
44+
}
45+
46+
$methodReflection = $this->getMethodReflection($expr, $scope);
47+
48+
if ($methodReflection === null) {
49+
return null;
50+
}
51+
52+
$returnType = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants())->getReturnType();
53+
54+
$returnsQueryBuilder = (new ObjectType(QueryBuilder::class))->isSuperTypeOf($returnType)->yes();
55+
56+
if (!$returnsQueryBuilder) {
57+
return null;
58+
}
59+
60+
$queryBuilderTypes = $this->otherMethodQueryBuilderParser->findQueryBuilderTypesInCalledMethod($scope, $methodReflection);
61+
if (count($queryBuilderTypes) === 0) {
62+
return null;
63+
}
64+
65+
return TypeCombinator::union(...$queryBuilderTypes);
66+
}
67+
68+
/**
69+
* @param StaticCall|MethodCall $call
70+
*/
71+
private function getMethodReflection(CallLike $call, Scope $scope): ?MethodReflection
72+
{
73+
if (!$call->name instanceof Identifier) {
74+
return null;
75+
}
76+
77+
if ($call instanceof MethodCall) {
78+
$callerType = $scope->getType($call->var);
79+
} else {
80+
if (!$call->class instanceof Name) {
81+
return null;
82+
}
83+
$callerType = $scope->resolveTypeByName($call->class);
84+
}
85+
86+
$methodName = $call->name->name;
87+
88+
foreach ($callerType->getObjectClassReflections() as $callerClassReflection) {
89+
if ($callerClassReflection->is(QueryBuilder::class)) {
90+
return null; // covered by QueryBuilderMethodDynamicReturnTypeExtension
91+
}
92+
if ($callerClassReflection->is(EntityRepository::class) && $methodName === 'createQueryBuilder') {
93+
return null; // covered by EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension
94+
}
95+
if ($callerClassReflection->is(EntityManagerInterface::class) && $methodName === 'createQueryBuilder') {
96+
return null; // no need to dive there
97+
}
98+
}
99+
100+
return $scope->getMethodReflection($callerType, $methodName);
101+
}
102+
103+
}

tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php

-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use PHPStan\Rules\Rule;
66
use PHPStan\Testing\RuleTestCase;
77
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
8-
use PHPStan\Type\Doctrine\QueryBuilder\OtherMethodQueryBuilderParser;
98

109
/**
1110
* @extends RuleTestCase<QueryBuilderDqlRule>
@@ -17,8 +16,6 @@ protected function getRule(): Rule
1716
{
1817
return new QueryBuilderDqlRule(
1918
new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', __DIR__ . '/../../../../tmp'),
20-
self::getContainer()->getByType(OtherMethodQueryBuilderParser::class),
21-
true,
2219
true
2320
);
2421
}

tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php

-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use PHPStan\Rules\Rule;
66
use PHPStan\Testing\RuleTestCase;
77
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
8-
use PHPStan\Type\Doctrine\QueryBuilder\OtherMethodQueryBuilderParser;
98

109
/**
1110
* @extends RuleTestCase<QueryBuilderDqlRule>
@@ -17,8 +16,6 @@ protected function getRule(): Rule
1716
{
1817
return new QueryBuilderDqlRule(
1918
new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', __DIR__ . '/../../../../tmp'),
20-
self::getContainer()->getByType(OtherMethodQueryBuilderParser::class),
21-
true,
2219
true
2320
);
2421
}

tests/Rules/Doctrine/ORM/data/query-builder-dql.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,14 @@ public function selectArray(): void
8484
->getQuery();
8585
}
8686

87-
public function analyseQueryBuilderUnknownBeginning(): void
87+
public function analyseQueryBuilderOtherMethodBeginning(): void
8888
{
8989
$this->createQb()->getQuery();
9090
}
9191

9292
private function createQb(): \Doctrine\ORM\QueryBuilder
9393
{
94-
return $this->entityManager->createQueryBuilder();
94+
return $this->entityManager->createQueryBuilder()->select('e')->from(MyEntity::class, 'e');
9595
}
9696

9797
public function analyseQueryBuilderDynamicArgs(string $entity): void

0 commit comments

Comments
 (0)