Skip to content

Commit 7460f9d

Browse files
Khartirondrejmirtes
authored andcommitted
determine queryBuilderType if called on other method
1 parent b7195d6 commit 7460f9d

File tree

6 files changed

+225
-179
lines changed

6 files changed

+225
-179
lines changed

Diff for: extension.neon

+5-2
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,7 @@ services:
110110
-
111111
class: PHPStan\Type\Doctrine\QueryBuilder\QueryBuilderMethodDynamicReturnTypeExtension
112112
arguments:
113-
parser: @defaultAnalysisParser
114113
queryBuilderClass: %doctrine.queryBuilderClass%
115-
descendIntoOtherMethods: %doctrine.searchOtherMethodsForQueryBuilderBeginning%
116114
tags:
117115
- phpstan.broker.dynamicMethodReturnTypeExtension
118116

@@ -147,6 +145,11 @@ services:
147145
class: PHPStan\Rules\Doctrine\ORM\PropertiesExtension
148146
tags:
149147
- phpstan.properties.readWriteExtension
148+
-
149+
class: PHPStan\Type\Doctrine\QueryBuilder\OtherMethodQueryBuilderParser
150+
arguments:
151+
descendIntoOtherMethods: %doctrine.searchOtherMethodsForQueryBuilderBeginning%
152+
parser: @defaultAnalysisParser
150153

151154
doctrineQueryBuilderArgumentsProcessor:
152155
class: PHPStan\Type\Doctrine\ArgumentsProcessor

Diff for: phpstan.neon

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ parameters:
2626
path: src/Type/Doctrine/QueryBuilder/Expr/ExpressionBuilderDynamicReturnTypeExtension.php
2727
-
2828
message: '~^Variable property access on PhpParser\\Node\\Stmt\\Declare_\|PhpParser\\Node\\Stmt\\Namespace_\.$~'
29-
path: src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php
29+
path: src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\QueryBuilder;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PhpParser\Node\Identifier;
8+
use PhpParser\Node\Stmt;
9+
use PhpParser\Node\Stmt\Class_;
10+
use PhpParser\Node\Stmt\ClassMethod;
11+
use PhpParser\Node\Stmt\Declare_;
12+
use PhpParser\Node\Stmt\Namespace_;
13+
use PhpParser\Node\Stmt\Return_;
14+
use PHPStan\Analyser\NodeScopeResolver;
15+
use PHPStan\Analyser\Scope;
16+
use PHPStan\Analyser\ScopeContext;
17+
use PHPStan\Analyser\ScopeFactory;
18+
use PHPStan\DependencyInjection\Container;
19+
use PHPStan\Parser\Parser;
20+
use PHPStan\Reflection\ReflectionProvider;
21+
use PHPStan\Type\Generic\TemplateTypeMap;
22+
use PHPStan\Type\TypeWithClassName;
23+
use function is_array;
24+
25+
class OtherMethodQueryBuilderParser
26+
{
27+
28+
/** @var bool */
29+
private $descendIntoOtherMethods;
30+
31+
/** @var ReflectionProvider */
32+
private $broker;
33+
34+
/** @var Parser */
35+
private $parser;
36+
37+
/** @var Container */
38+
private $container;
39+
40+
public function __construct(bool $descendIntoOtherMethods, ReflectionProvider $broker, Parser $parser, Container $container)
41+
{
42+
$this->descendIntoOtherMethods = $descendIntoOtherMethods;
43+
$this->broker = $broker;
44+
$this->parser = $parser;
45+
$this->container = $container;
46+
}
47+
48+
/**
49+
* @return QueryBuilderType[]
50+
*/
51+
public function getQueryBuilderTypes(Scope $scope, MethodCall $methodCall): array
52+
{
53+
if (!$this->descendIntoOtherMethods || !$methodCall->var instanceof MethodCall) {
54+
return [];
55+
}
56+
57+
return $this->findQueryBuilderTypesInCalledMethod($scope, $methodCall->var);
58+
}
59+
/**
60+
* @return QueryBuilderType[]
61+
*/
62+
private function findQueryBuilderTypesInCalledMethod(Scope $scope, MethodCall $methodCall): array
63+
{
64+
$methodCalledOnType = $scope->getType($methodCall->var);
65+
if (!$methodCall->name instanceof Identifier) {
66+
return [];
67+
}
68+
69+
if (!$methodCalledOnType instanceof TypeWithClassName) {
70+
return [];
71+
}
72+
73+
if (!$this->broker->hasClass($methodCalledOnType->getClassName())) {
74+
return [];
75+
}
76+
77+
$classReflection = $this->broker->getClass($methodCalledOnType->getClassName());
78+
$methodName = $methodCall->name->toString();
79+
if (!$classReflection->hasNativeMethod($methodName)) {
80+
return [];
81+
}
82+
83+
$methodReflection = $classReflection->getNativeMethod($methodName);
84+
$fileName = $methodReflection->getDeclaringClass()->getFileName();
85+
if ($fileName === null) {
86+
return [];
87+
}
88+
89+
$nodes = $this->parser->parseFile($fileName);
90+
$classNode = $this->findClassNode($methodReflection->getDeclaringClass()->getName(), $nodes);
91+
if ($classNode === null) {
92+
return [];
93+
}
94+
95+
$methodNode = $this->findMethodNode($methodReflection->getName(), $classNode->stmts);
96+
if ($methodNode === null || $methodNode->stmts === null) {
97+
return [];
98+
}
99+
100+
/** @var NodeScopeResolver $nodeScopeResolver */
101+
$nodeScopeResolver = $this->container->getByType(NodeScopeResolver::class);
102+
103+
/** @var ScopeFactory $scopeFactory */
104+
$scopeFactory = $this->container->getByType(ScopeFactory::class);
105+
106+
$methodScope = $scopeFactory->create(
107+
ScopeContext::create($fileName),
108+
$scope->isDeclareStrictTypes(),
109+
[],
110+
$methodReflection,
111+
$scope->getNamespace()
112+
)->enterClass($methodReflection->getDeclaringClass())->enterClassMethod($methodNode, TemplateTypeMap::createEmpty(), [], null, null, null, false, false, false);
113+
114+
$queryBuilderTypes = [];
115+
116+
$nodeScopeResolver->processNodes($methodNode->stmts, $methodScope, static function (Node $node, Scope $scope) use (&$queryBuilderTypes): void {
117+
if (!$node instanceof Return_ || $node->expr === null) {
118+
return;
119+
}
120+
121+
$exprType = $scope->getType($node->expr);
122+
if (!$exprType instanceof QueryBuilderType) {
123+
return;
124+
}
125+
126+
$queryBuilderTypes[] = $exprType;
127+
});
128+
129+
return $queryBuilderTypes;
130+
}
131+
132+
/**
133+
* @param Node[] $nodes
134+
*/
135+
private function findClassNode(string $className, array $nodes): ?Class_
136+
{
137+
foreach ($nodes as $node) {
138+
if (
139+
$node instanceof Class_
140+
&& $node->namespacedName !== null
141+
&& $node->namespacedName->toString() === $className
142+
) {
143+
return $node;
144+
}
145+
146+
if (
147+
!$node instanceof Namespace_
148+
&& !$node instanceof Declare_
149+
) {
150+
continue;
151+
}
152+
$subNodeNames = $node->getSubNodeNames();
153+
foreach ($subNodeNames as $subNodeName) {
154+
$subNode = $node->{$subNodeName};
155+
if (!is_array($subNode)) {
156+
$subNode = [$subNode];
157+
}
158+
159+
$result = $this->findClassNode($className, $subNode);
160+
if ($result === null) {
161+
continue;
162+
}
163+
164+
return $result;
165+
}
166+
}
167+
168+
return null;
169+
}
170+
171+
/**
172+
* @param Stmt[] $classStatements
173+
*/
174+
private function findMethodNode(string $methodName, array $classStatements): ?ClassMethod
175+
{
176+
foreach ($classStatements as $statement) {
177+
if (
178+
$statement instanceof ClassMethod
179+
&& $statement->name->toString() === $methodName
180+
) {
181+
return $statement;
182+
}
183+
}
184+
185+
return null;
186+
}
187+
188+
}

Diff for: src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php

+10-2
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,22 @@ class QueryBuilderGetQueryDynamicReturnTypeExtension implements DynamicMethodRet
4242
/** @var DescriptorRegistry */
4343
private $descriptorRegistry;
4444

45+
/** @var OtherMethodQueryBuilderParser */
46+
private $otherMethodQueryBuilderParser;
47+
4548
public function __construct(
4649
ObjectMetadataResolver $objectMetadataResolver,
4750
ArgumentsProcessor $argumentsProcessor,
4851
?string $queryBuilderClass,
49-
DescriptorRegistry $descriptorRegistry
52+
DescriptorRegistry $descriptorRegistry,
53+
OtherMethodQueryBuilderParser $otherMethodQueryBuilderParser
5054
)
5155
{
5256
$this->objectMetadataResolver = $objectMetadataResolver;
5357
$this->argumentsProcessor = $argumentsProcessor;
5458
$this->queryBuilderClass = $queryBuilderClass;
5559
$this->descriptorRegistry = $descriptorRegistry;
60+
$this->otherMethodQueryBuilderParser = $otherMethodQueryBuilderParser;
5661
}
5762

5863
public function getClass(): string
@@ -79,7 +84,10 @@ public function getTypeFromMethodCall(
7984
)->getReturnType();
8085
$queryBuilderTypes = DoctrineTypeUtils::getQueryBuilderTypes($calledOnType);
8186
if (count($queryBuilderTypes) === 0) {
82-
return $defaultReturnType;
87+
$queryBuilderTypes = $this->otherMethodQueryBuilderParser->getQueryBuilderTypes($scope, $methodCall);
88+
if (count($queryBuilderTypes) === 0) {
89+
return $defaultReturnType;
90+
}
8391
}
8492

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

0 commit comments

Comments
 (0)