Skip to content

Commit 6827950

Browse files
committed
Merge remote-tracking branch 'origin/1.3.x' into 1.4.x
2 parents 24adb0e + 03b7d3f commit 6827950

13 files changed

+285
-120
lines changed

extension.neon

Lines changed: 5 additions & 0 deletions
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

Lines changed: 0 additions & 1 deletion
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

Lines changed: 1 addition & 20 deletions
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

Lines changed: 25 additions & 42 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,88 +15,66 @@
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;
25+
use function sprintf;
2826

2927
class OtherMethodQueryBuilderParser
3028
{
3129

3230
/** @var bool */
3331
private $descendIntoOtherMethods;
3432

35-
/** @var ReflectionProvider */
36-
private $reflectionProvider;
37-
3833
/** @var Parser */
3934
private $parser;
4035

4136
/** @var Container */
4237
private $container;
4338

44-
public function __construct(bool $descendIntoOtherMethods, ReflectionProvider $reflectionProvider, Parser $parser, Container $container)
39+
/** @var array<string, list<QueryBuilderType>> */
40+
private $cache = [];
41+
42+
public function __construct(bool $descendIntoOtherMethods, Parser $parser, Container $container)
4543
{
4644
$this->descendIntoOtherMethods = $descendIntoOtherMethods;
47-
$this->reflectionProvider = $reflectionProvider;
4845
$this->parser = $parser;
4946
$this->container = $container;
5047
}
5148

5249
/**
53-
* @return QueryBuilderType[]
54-
*/
55-
public function getQueryBuilderTypes(Scope $scope, MethodCall $methodCall): array
56-
{
57-
if (!$this->descendIntoOtherMethods || !$methodCall->var instanceof MethodCall) {
58-
return [];
59-
}
60-
61-
return $this->findQueryBuilderTypesInCalledMethod($scope, $methodCall->var);
62-
}
63-
/**
64-
* @return QueryBuilderType[]
50+
* @return list<QueryBuilderType>
6551
*/
66-
private function findQueryBuilderTypesInCalledMethod(Scope $scope, MethodCall $methodCall): array
52+
public function findQueryBuilderTypesInCalledMethod(Scope $scope, MethodReflection $methodReflection): array
6753
{
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) {
54+
if (!$this->descendIntoOtherMethods) {
7655
return [];
7756
}
7857

79-
if (!$this->reflectionProvider->hasClass($methodCalledOnTypeClassNames[0])) {
58+
$methodName = $methodReflection->getName();
59+
$className = $methodReflection->getDeclaringClass()->getName();
60+
$fileName = $methodReflection->getDeclaringClass()->getFileName();
61+
if ($fileName === null) {
8062
return [];
8163
}
8264

83-
$classReflection = $this->reflectionProvider->getClass($methodCalledOnTypeClassNames[0]);
84-
$methodName = $methodCall->name->toString();
85-
if (!$classReflection->hasNativeMethod($methodName)) {
86-
return [];
87-
}
65+
$cacheKey = $this->buildCacheKey($fileName, $className, $methodName);
8866

89-
$methodReflection = $classReflection->getNativeMethod($methodName);
90-
$fileName = $methodReflection->getDeclaringClass()->getFileName();
91-
if ($fileName === null) {
92-
return [];
67+
if (isset($this->cache[$cacheKey])) {
68+
return $this->cache[$cacheKey];
9369
}
9470

9571
$nodes = $this->parser->parseFile($fileName);
96-
$classNode = $this->findClassNode($methodReflection->getDeclaringClass()->getName(), $nodes);
72+
$classNode = $this->findClassNode($className, $nodes);
9773
if ($classNode === null) {
9874
return [];
9975
}
10076

101-
$methodNode = $this->findMethodNode($methodReflection->getName(), $classNode->stmts);
77+
$methodNode = $this->findMethodNode($methodName, $classNode->stmts);
10278
if ($methodNode === null || $methodNode->stmts === null) {
10379
return [];
10480
}
@@ -136,6 +112,8 @@ private function findQueryBuilderTypesInCalledMethod(Scope $scope, MethodCall $m
136112
});
137113
});
138114

115+
$this->cache[$cacheKey] = $queryBuilderTypes;
116+
139117
return $queryBuilderTypes;
140118
}
141119

@@ -195,4 +173,9 @@ private function findMethodNode(string $methodName, array $classStatements): ?Cl
195173
return null;
196174
}
197175

176+
private function buildCacheKey(string $fileName, string $declaringClassName, string $methodName): string
177+
{
178+
return sprintf('%s-%s-%s', $fileName, $declaringClassName, $methodName);
179+
}
180+
198181
}

src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,22 +66,17 @@ class QueryBuilderGetQueryDynamicReturnTypeExtension implements DynamicMethodRet
6666
/** @var DescriptorRegistry */
6767
private $descriptorRegistry;
6868

69-
/** @var OtherMethodQueryBuilderParser */
70-
private $otherMethodQueryBuilderParser;
71-
7269
public function __construct(
7370
ObjectMetadataResolver $objectMetadataResolver,
7471
ArgumentsProcessor $argumentsProcessor,
7572
?string $queryBuilderClass,
76-
DescriptorRegistry $descriptorRegistry,
77-
OtherMethodQueryBuilderParser $otherMethodQueryBuilderParser
73+
DescriptorRegistry $descriptorRegistry
7874
)
7975
{
8076
$this->objectMetadataResolver = $objectMetadataResolver;
8177
$this->argumentsProcessor = $argumentsProcessor;
8278
$this->queryBuilderClass = $queryBuilderClass;
8379
$this->descriptorRegistry = $descriptorRegistry;
84-
$this->otherMethodQueryBuilderParser = $otherMethodQueryBuilderParser;
8580
}
8681

8782
public function getClass(): string
@@ -108,10 +103,7 @@ public function getTypeFromMethodCall(
108103
)->getReturnType();
109104
$queryBuilderTypes = DoctrineTypeUtils::getQueryBuilderTypes($calledOnType);
110105
if (count($queryBuilderTypes) === 0) {
111-
$queryBuilderTypes = $this->otherMethodQueryBuilderParser->getQueryBuilderTypes($scope, $methodCall);
112-
if (count($queryBuilderTypes) === 0) {
113-
return $defaultReturnType;
114-
}
106+
return $defaultReturnType;
115107
}
116108

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

src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php

Lines changed: 2 additions & 10 deletions
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) {
Lines changed: 103 additions & 0 deletions
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

Lines changed: 0 additions & 3 deletions
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
}

0 commit comments

Comments
 (0)