22
33namespace PHPStan \Type \Doctrine \QueryBuilder ;
44
5+ use PhpParser \Node ;
56use PhpParser \Node \Expr \MethodCall ;
67use 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 ;
714use 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 ;
821use PHPStan \Reflection \MethodReflection ;
922use PHPStan \Reflection \ParametersAcceptorSelector ;
1023use PHPStan \Type \Doctrine \DoctrineTypeUtils ;
1124use PHPStan \Type \MixedType ;
1225use PHPStan \Type \ObjectType ;
1326use PHPStan \Type \Type ;
1427use PHPStan \Type \TypeCombinator ;
28+ use PHPStan \Type \TypeWithClassName ;
1529
16- class QueryBuilderMethodDynamicReturnTypeExtension implements \PHPStan \Type \DynamicMethodReturnTypeExtension
30+ class QueryBuilderMethodDynamicReturnTypeExtension implements \PHPStan \Type \DynamicMethodReturnTypeExtension, BrokerAwareExtension
1731{
1832
1933 private const MAX_COMBINATIONS = 16 ;
2034
35+ /** @var \PHPStan\DependencyInjection\Container */
36+ private $ container ;
37+
38+ /** @var \PHPStan\Parser\Parser */
39+ private $ parser ;
40+
2141 /** @var string|null */
2242 private $ queryBuilderClass ;
2343
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+ )
2556 {
57+ $ this ->container = $ container ;
58+ $ this ->parser = $ parser ;
2659 $ this ->queryBuilderClass = $ queryBuilderClass ;
60+ $ this ->descendIntoOtherMethods = $ descendIntoOtherMethods ;
61+ }
62+
63+ public function setBroker (Broker $ broker ): void
64+ {
65+ $ this ->broker = $ broker ;
2766 }
2867
2968 public function getClass (): string
@@ -62,8 +101,16 @@ public function getTypeFromMethodCall(
62101
63102 $ queryBuilderTypes = DoctrineTypeUtils::getQueryBuilderTypes ($ calledOnType );
64103 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+ }
66112 }
113+
67114 if (count ($ queryBuilderTypes ) > self ::MAX_COMBINATIONS ) {
68115 return $ calledOnType ;
69116 }
@@ -76,4 +123,137 @@ public function getTypeFromMethodCall(
76123 return TypeCombinator::union (...$ resultTypes );
77124 }
78125
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+
79259}
0 commit comments