2
2
3
3
namespace PHPStan \Type \Doctrine \QueryBuilder ;
4
4
5
+ use PhpParser \Node ;
5
6
use PhpParser \Node \Expr \MethodCall ;
6
7
use PhpParser \Node \Identifier ;
8
+ use PhpParser \Node \Stmt \Class_ ;
9
+ use PhpParser \Node \Stmt \ClassMethod ;
10
+ use PhpParser \Node \Stmt \Declare_ ;
11
+ use PhpParser \Node \Stmt \Namespace_ ;
12
+ use PhpParser \Node \Stmt \Return_ ;
13
+ use PHPStan \Analyser \NodeScopeResolver ;
7
14
use PHPStan \Analyser \Scope ;
15
+ use PHPStan \Analyser \ScopeContext ;
16
+ use PHPStan \Analyser \ScopeFactory ;
17
+ use PHPStan \Broker \Broker ;
18
+ use PHPStan \DependencyInjection \Container ;
19
+ use PHPStan \Parser \Parser ;
20
+ use PHPStan \Reflection \BrokerAwareExtension ;
8
21
use PHPStan \Reflection \MethodReflection ;
9
22
use PHPStan \Reflection \ParametersAcceptorSelector ;
10
23
use PHPStan \Type \Doctrine \DoctrineTypeUtils ;
11
24
use PHPStan \Type \MixedType ;
12
25
use PHPStan \Type \ObjectType ;
13
26
use PHPStan \Type \Type ;
14
27
use PHPStan \Type \TypeCombinator ;
28
+ use PHPStan \Type \TypeWithClassName ;
15
29
16
- class QueryBuilderMethodDynamicReturnTypeExtension implements \PHPStan \Type \DynamicMethodReturnTypeExtension
30
+ class QueryBuilderMethodDynamicReturnTypeExtension implements \PHPStan \Type \DynamicMethodReturnTypeExtension, BrokerAwareExtension
17
31
{
18
32
19
33
private const MAX_COMBINATIONS = 16 ;
20
34
35
+ /** @var \PHPStan\DependencyInjection\Container */
36
+ private $ container ;
37
+
38
+ /** @var \PHPStan\Parser\Parser */
39
+ private $ parser ;
40
+
21
41
/** @var string|null */
22
42
private $ queryBuilderClass ;
23
43
24
- public function __construct (?string $ queryBuilderClass )
44
+ /** @var bool */
45
+ private $ descendIntoOtherMethods ;
46
+
47
+ /** @var \PHPStan\Broker\Broker */
48
+ private $ broker ;
49
+
50
+ public function __construct (
51
+ Container $ container ,
52
+ Parser $ parser ,
53
+ ?string $ queryBuilderClass ,
54
+ bool $ descendIntoOtherMethods
55
+ )
25
56
{
57
+ $ this ->container = $ container ;
58
+ $ this ->parser = $ parser ;
26
59
$ this ->queryBuilderClass = $ queryBuilderClass ;
60
+ $ this ->descendIntoOtherMethods = $ descendIntoOtherMethods ;
61
+ }
62
+
63
+ public function setBroker (Broker $ broker ): void
64
+ {
65
+ $ this ->broker = $ broker ;
27
66
}
28
67
29
68
public function getClass (): string
@@ -62,8 +101,16 @@ public function getTypeFromMethodCall(
62
101
63
102
$ queryBuilderTypes = DoctrineTypeUtils::getQueryBuilderTypes ($ calledOnType );
64
103
if (count ($ queryBuilderTypes ) === 0 ) {
65
- return $ calledOnType ;
104
+ if (!$ this ->descendIntoOtherMethods || !$ methodCall ->var instanceof MethodCall) {
105
+ return $ calledOnType ;
106
+ }
107
+
108
+ $ queryBuilderTypes = $ this ->findQueryBuilderTypesInCalledMethod ($ scope , $ methodCall ->var );
109
+ if (count ($ queryBuilderTypes ) === 0 ) {
110
+ return $ calledOnType ;
111
+ }
66
112
}
113
+
67
114
if (count ($ queryBuilderTypes ) > self ::MAX_COMBINATIONS ) {
68
115
return $ calledOnType ;
69
116
}
@@ -76,4 +123,137 @@ public function getTypeFromMethodCall(
76
123
return TypeCombinator::union (...$ resultTypes );
77
124
}
78
125
126
+ /**
127
+ * @param \PHPStan\Analyser\Scope $scope
128
+ * @param \PhpParser\Node\Expr\MethodCall $methodCall
129
+ * @return \PHPStan\Type\Doctrine\QueryBuilder\QueryBuilderType[]
130
+ */
131
+ private function findQueryBuilderTypesInCalledMethod (Scope $ scope , MethodCall $ methodCall ): array
132
+ {
133
+ $ methodCalledOnType = $ scope ->getType ($ methodCall ->var );
134
+ if (!$ methodCall ->name instanceof Identifier) {
135
+ return [];
136
+ }
137
+
138
+ if (!$ methodCalledOnType instanceof TypeWithClassName) {
139
+ return [];
140
+ }
141
+
142
+ if (!$ this ->broker ->hasClass ($ methodCalledOnType ->getClassName ())) {
143
+ return [];
144
+ }
145
+
146
+ $ classReflection = $ this ->broker ->getClass ($ methodCalledOnType ->getClassName ());
147
+ $ methodName = $ methodCall ->name ->toString ();
148
+ if (!$ classReflection ->hasNativeMethod ($ methodName )) {
149
+ return [];
150
+ }
151
+
152
+ $ methodReflection = $ classReflection ->getNativeMethod ($ methodName );
153
+ $ fileName = $ methodReflection ->getDeclaringClass ()->getFileName ();
154
+ if ($ fileName === false ) {
155
+ return [];
156
+ }
157
+
158
+ $ nodes = $ this ->parser ->parseFile ($ fileName );
159
+ $ classNode = $ this ->findClassNode ($ methodReflection ->getDeclaringClass ()->getName (), $ nodes );
160
+ if ($ classNode === null ) {
161
+ return [];
162
+ }
163
+
164
+ $ methodNode = $ this ->findMethodNode ($ methodReflection ->getName (), $ classNode ->stmts );
165
+ if ($ methodNode === null || $ methodNode ->stmts === null ) {
166
+ return [];
167
+ }
168
+
169
+ /** @var \PHPStan\Analyser\NodeScopeResolver $nodeScopeResolver */
170
+ $ nodeScopeResolver = $ this ->container ->getByType (NodeScopeResolver::class);
171
+
172
+ /** @var \PHPStan\Analyser\ScopeFactory $scopeFactory */
173
+ $ scopeFactory = $ this ->container ->getByType (ScopeFactory::class);
174
+
175
+ $ methodScope = $ scopeFactory ->create (
176
+ ScopeContext::create ($ fileName ),
177
+ $ scope ->isDeclareStrictTypes (),
178
+ $ methodReflection ,
179
+ $ scope ->getNamespace ()
180
+ )->enterClass ($ methodReflection ->getDeclaringClass ())->enterClassMethod ($ methodNode , [], null , null , false , false , false );
181
+
182
+ $ queryBuilderTypes = [];
183
+
184
+ $ nodeScopeResolver ->processNodes ($ methodNode ->stmts , $ methodScope , function (Node $ node , Scope $ scope ) use (&$ queryBuilderTypes ): void {
185
+ if (!$ node instanceof Return_ || $ node ->expr === null ) {
186
+ return ;
187
+ }
188
+
189
+ $ exprType = $ scope ->getType ($ node ->expr );
190
+ if (!$ exprType instanceof QueryBuilderType) {
191
+ return ;
192
+ }
193
+
194
+ $ queryBuilderTypes [] = $ exprType ;
195
+ });
196
+
197
+ return $ queryBuilderTypes ;
198
+ }
199
+
200
+ /**
201
+ * @param string $className
202
+ * @param \PhpParser\Node[] $nodes
203
+ * @return \PhpParser\Node\Stmt\Class_|null
204
+ */
205
+ private function findClassNode (string $ className , array $ nodes ): ?Class_
206
+ {
207
+ foreach ($ nodes as $ node ) {
208
+ if (
209
+ $ node instanceof Class_
210
+ && $ node ->namespacedName ->toString () === $ className
211
+ ) {
212
+ return $ node ;
213
+ }
214
+
215
+ if (
216
+ !$ node instanceof Namespace_
217
+ && !$ node instanceof Declare_
218
+ ) {
219
+ continue ;
220
+ }
221
+ $ subNodeNames = $ node ->getSubNodeNames ();
222
+ foreach ($ subNodeNames as $ subNodeName ) {
223
+ $ subNode = $ node ->{$ subNodeName };
224
+ if (!is_array ($ subNode )) {
225
+ $ subNode = [$ subNode ];
226
+ }
227
+
228
+ $ result = $ this ->findClassNode ($ className , $ subNode );
229
+ if ($ result === null ) {
230
+ continue ;
231
+ }
232
+
233
+ return $ result ;
234
+ }
235
+ }
236
+
237
+ return null ;
238
+ }
239
+
240
+ /**
241
+ * @param string $methodName
242
+ * @param \PhpParser\Node\Stmt[] $classStatements
243
+ * @return \PhpParser\Node\Stmt\ClassMethod|null
244
+ */
245
+ private function findMethodNode (string $ methodName , array $ classStatements ): ?ClassMethod
246
+ {
247
+ foreach ($ classStatements as $ statement ) {
248
+ if (
249
+ $ statement instanceof ClassMethod
250
+ && $ statement ->name ->toString () === $ methodName
251
+ ) {
252
+ return $ statement ;
253
+ }
254
+ }
255
+
256
+ return null ;
257
+ }
258
+
79
259
}
0 commit comments