1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
/** |
4
|
|
|
* Parser Reflection API |
5
|
|
|
* |
6
|
|
|
* @copyright Copyright 2015, Lisachenko Alexander <[email protected]> |
7
|
|
|
* |
8
|
|
|
* This source file is subject to the license that is bundled |
9
|
|
|
* with this source code in the file LICENSE. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Go\ParserReflection; |
13
|
|
|
|
14
|
|
|
use Go\ParserReflection\Instrument\PathResolver; |
15
|
|
|
use Go\ParserReflection\ValueResolver\NodeExpressionResolver; |
16
|
|
|
use PhpParser\Node; |
17
|
|
|
use PhpParser\Node\Expr\FuncCall; |
18
|
|
|
use PhpParser\Node\Name; |
19
|
|
|
use PhpParser\Node\Stmt\ClassLike; |
20
|
|
|
use PhpParser\Node\Stmt\Const_; |
21
|
|
|
use PhpParser\Node\Stmt\Expression; |
22
|
|
|
use PhpParser\Node\Stmt\Function_; |
23
|
|
|
use PhpParser\Node\Stmt\Namespace_; |
24
|
|
|
use PhpParser\Node\Stmt\Use_; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* AST-based reflection for the concrete namespace in the file |
28
|
|
|
*/ |
29
|
|
|
class ReflectionFileNamespace |
30
|
|
|
{ |
31
|
|
|
/** |
32
|
|
|
* List of classes in the namespace |
33
|
|
|
* |
34
|
|
|
* @var array|ReflectionClass[] |
35
|
|
|
*/ |
36
|
|
|
protected $fileClasses; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* List of functions in the namespace |
40
|
|
|
* |
41
|
|
|
* @var array|ReflectionFunction[] |
42
|
|
|
*/ |
43
|
|
|
protected $fileFunctions; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* List of constants in the namespace |
47
|
|
|
* |
48
|
|
|
* @var array |
49
|
|
|
*/ |
50
|
|
|
protected $fileConstants; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* List of constants in the namespace including defined via "define(...)" |
54
|
|
|
* |
55
|
|
|
* @var array |
56
|
|
|
*/ |
57
|
|
|
protected $fileConstantsWithDefined; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* List of imported namespaces (aliases) |
61
|
|
|
* |
62
|
|
|
* @var array |
63
|
|
|
*/ |
64
|
|
|
protected $fileNamespaceAliases; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Namespace node |
68
|
|
|
* |
69
|
|
|
* @var Namespace_ |
70
|
|
|
*/ |
71
|
|
|
private $namespaceNode; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Name of the file |
75
|
|
|
* |
76
|
|
|
* @var string |
77
|
|
|
*/ |
78
|
|
|
private $fileName; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* File namespace constructor |
82
|
|
|
* |
83
|
|
|
* @param string $fileName Name of the file |
84
|
|
|
* @param string $namespaceName Name of the namespace |
85
|
|
|
* @param Namespace_|null $namespaceNode Optional AST-node for this namespace block |
86
|
|
|
*/ |
87
|
3040 |
|
public function __construct(string $fileName, string $namespaceName, ?Namespace_ $namespaceNode = null) |
88
|
|
|
{ |
89
|
3040 |
|
$fileName = PathResolver::realpath($fileName); |
90
|
3040 |
|
if (!$namespaceNode) { |
91
|
24 |
|
$namespaceNode = ReflectionEngine::parseFileNamespace($fileName, $namespaceName); |
92
|
|
|
} |
93
|
3040 |
|
$this->namespaceNode = $namespaceNode; |
94
|
3040 |
|
$this->fileName = $fileName; |
|
|
|
|
95
|
3040 |
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Returns the concrete class from the file namespace or false if there is no class |
99
|
|
|
* |
100
|
|
|
* @return bool|ReflectionClass |
101
|
|
|
*/ |
102
|
3003 |
|
public function getClass(string $className) |
103
|
|
|
{ |
104
|
3003 |
|
if ($this->hasClass($className)) { |
105
|
3003 |
|
return $this->fileClasses[$className]; |
106
|
|
|
} |
107
|
|
|
|
108
|
10 |
|
return false; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Gets list of classes in the namespace |
113
|
|
|
* |
114
|
|
|
* @return ReflectionClass[] |
115
|
|
|
*/ |
116
|
3007 |
|
public function getClasses(): array |
117
|
|
|
{ |
118
|
3007 |
|
if (!isset($this->fileClasses)) { |
119
|
3007 |
|
$this->fileClasses = $this->findClasses(); |
120
|
|
|
} |
121
|
|
|
|
122
|
3007 |
|
return $this->fileClasses; |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Returns a value for the constant |
127
|
|
|
* |
128
|
|
|
* @return bool|mixed |
129
|
|
|
*/ |
130
|
15 |
|
public function getConstant(string $constantName) |
131
|
|
|
{ |
132
|
15 |
|
if ($this->hasConstant($constantName)) { |
133
|
15 |
|
return $this->fileConstants[$constantName]; |
134
|
|
|
} |
135
|
|
|
|
136
|
2 |
|
return false; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Returns a list of defined constants in the namespace |
141
|
|
|
* |
142
|
|
|
* @param bool $withDefined Include constants defined via "define(...)" in results. |
143
|
|
|
* |
144
|
|
|
* @return array |
145
|
|
|
*/ |
146
|
28 |
|
public function getConstants(bool $withDefined = false): array |
147
|
|
|
{ |
148
|
28 |
|
if ($withDefined) { |
149
|
2 |
|
if (!isset($this->fileConstantsWithDefined)) { |
150
|
2 |
|
$this->fileConstantsWithDefined = $this->findConstants(true); |
151
|
|
|
} |
152
|
|
|
|
153
|
2 |
|
return $this->fileConstantsWithDefined; |
154
|
|
|
} |
155
|
|
|
|
156
|
27 |
|
if (!isset($this->fileConstants)) { |
157
|
27 |
|
$this->fileConstants = $this->findConstants(); |
158
|
|
|
} |
159
|
|
|
|
160
|
27 |
|
return $this->fileConstants; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* Gets doc comments from a class. |
165
|
|
|
* |
166
|
|
|
* @return string|false The doc comment if it exists, otherwise "false" |
167
|
|
|
*/ |
168
|
1 |
|
public function getDocComment() |
169
|
|
|
{ |
170
|
1 |
|
$docComment = false; |
171
|
1 |
|
$comments = $this->namespaceNode->getAttribute('comments'); |
172
|
|
|
|
173
|
1 |
|
if ($comments) { |
174
|
1 |
|
$docComment = (string)$comments[0]; |
175
|
|
|
} |
176
|
|
|
|
177
|
1 |
|
return $docComment; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* Gets starting line number |
182
|
|
|
*/ |
183
|
1 |
|
public function getEndLine(): int |
184
|
|
|
{ |
185
|
1 |
|
return $this->namespaceNode->getAttribute('endLine'); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Returns the name of file |
190
|
|
|
*/ |
191
|
4 |
|
public function getFileName(): string |
192
|
|
|
{ |
193
|
4 |
|
return $this->fileName; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Returns the concrete function from the file namespace or false if there is no function |
198
|
|
|
* |
199
|
|
|
* @return bool|ReflectionFunction |
200
|
|
|
*/ |
201
|
9 |
|
public function getFunction(string $functionName) |
202
|
|
|
{ |
203
|
9 |
|
if ($this->hasFunction($functionName)) { |
204
|
9 |
|
return $this->fileFunctions[$functionName]; |
205
|
|
|
} |
206
|
|
|
|
207
|
1 |
|
return false; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Gets list of functions in the namespace |
212
|
|
|
* |
213
|
|
|
* @return ReflectionFunction[] |
214
|
|
|
*/ |
215
|
17 |
|
public function getFunctions(): array |
216
|
|
|
{ |
217
|
17 |
|
if (!isset($this->fileFunctions)) { |
218
|
17 |
|
$this->fileFunctions = $this->findFunctions(); |
219
|
|
|
} |
220
|
|
|
|
221
|
17 |
|
return $this->fileFunctions; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Gets namespace name |
226
|
|
|
*/ |
227
|
3029 |
|
public function getName(): string |
228
|
|
|
{ |
229
|
3029 |
|
$nameNode = $this->namespaceNode->name; |
230
|
|
|
|
231
|
3029 |
|
return $nameNode ? $nameNode->toString() : ''; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Returns a list of namespace aliases |
236
|
|
|
*/ |
237
|
1 |
|
public function getNamespaceAliases(): array |
238
|
|
|
{ |
239
|
1 |
|
if (!isset($this->fileNamespaceAliases)) { |
240
|
1 |
|
$this->fileNamespaceAliases = $this->findNamespaceAliases(); |
241
|
|
|
} |
242
|
|
|
|
243
|
1 |
|
return $this->fileNamespaceAliases; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* Returns an AST-node for namespace |
248
|
|
|
*/ |
249
|
|
|
public function getNode(): ?Namespace_ |
250
|
|
|
{ |
251
|
|
|
return $this->namespaceNode; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* Helper method to access last token position for namespace |
256
|
|
|
* |
257
|
|
|
* This method is useful because namespace can be declared with braces or without them |
258
|
|
|
*/ |
259
|
|
|
public function getLastTokenPosition() |
260
|
|
|
{ |
261
|
|
|
$endNamespaceTokenPosition = $this->namespaceNode->getAttribute('endTokenPos'); |
262
|
|
|
|
263
|
|
|
/** @var Node $lastNamespaceNode */ |
264
|
|
|
$lastNamespaceNode = end($this->namespaceNode->stmts); |
265
|
|
|
$endStatementTokenPosition = $lastNamespaceNode->getAttribute('endTokenPos'); |
266
|
|
|
|
267
|
|
|
return max($endNamespaceTokenPosition, $endStatementTokenPosition); |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
* Gets starting line number |
272
|
|
|
*/ |
273
|
1 |
|
public function getStartLine(): int |
274
|
|
|
{ |
275
|
1 |
|
return $this->namespaceNode->getAttribute('startLine'); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* Checks if the given class is present in this file namespace |
280
|
|
|
*/ |
281
|
3004 |
|
public function hasClass(string $className): bool |
282
|
|
|
{ |
283
|
3004 |
|
$classes = $this->getClasses(); |
284
|
|
|
|
285
|
3004 |
|
return isset($classes[$className]); |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
/** |
289
|
|
|
* Checks if the given constant is present in this file namespace |
290
|
|
|
*/ |
291
|
27 |
|
public function hasConstant(string $constantName): bool |
292
|
|
|
{ |
293
|
27 |
|
$constants = $this->getConstants(); |
294
|
|
|
|
295
|
27 |
|
return isset($constants[$constantName]); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* Checks if the given function is present in this file namespace |
300
|
|
|
*/ |
301
|
10 |
|
public function hasFunction(string $functionName): bool |
302
|
|
|
{ |
303
|
10 |
|
$functions = $this->getFunctions(); |
304
|
|
|
|
305
|
10 |
|
return isset($functions[$functionName]); |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* Searches for classes in the given AST |
310
|
|
|
* |
311
|
|
|
* @return ReflectionClass[] |
312
|
|
|
*/ |
313
|
3007 |
|
private function findClasses(): array |
314
|
|
|
{ |
315
|
3007 |
|
$classes = []; |
316
|
3007 |
|
$namespaceName = $this->getName(); |
317
|
|
|
// classes can be only top-level nodes in the namespace, so we can scan them directly |
318
|
3007 |
|
foreach ($this->namespaceNode->stmts as $namespaceLevelNode) { |
319
|
3007 |
|
if ($namespaceLevelNode instanceof ClassLike) { |
320
|
3007 |
|
$classShortName = $namespaceLevelNode->name->toString(); |
321
|
3007 |
|
$className = $namespaceName ? $namespaceName .'\\' . $classShortName : $classShortName; |
322
|
|
|
|
323
|
3007 |
|
$namespaceLevelNode->setAttribute('fileName', $this->fileName); |
324
|
3007 |
|
$classes[$className] = new ReflectionClass($className, $namespaceLevelNode); |
325
|
|
|
} |
326
|
|
|
} |
327
|
|
|
|
328
|
3007 |
|
return $classes; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
/** |
332
|
|
|
* Searches for functions in the given AST |
333
|
|
|
* |
334
|
|
|
* @return ReflectionFunction[] |
335
|
|
|
*/ |
336
|
17 |
|
private function findFunctions(): array |
337
|
|
|
{ |
338
|
17 |
|
$functions = []; |
339
|
17 |
|
$namespaceName = $this->getName(); |
340
|
|
|
|
341
|
|
|
// functions can be only top-level nodes in the namespace, so we can scan them directly |
342
|
17 |
|
foreach ($this->namespaceNode->stmts as $namespaceLevelNode) { |
343
|
17 |
|
if ($namespaceLevelNode instanceof Function_) { |
344
|
17 |
|
$funcShortName = $namespaceLevelNode->name->toString(); |
345
|
17 |
|
$functionName = $namespaceName ? $namespaceName .'\\' . $funcShortName : $funcShortName; |
346
|
|
|
|
347
|
17 |
|
$namespaceLevelNode->setAttribute('fileName', $this->fileName); |
348
|
17 |
|
$functions[$funcShortName] = new ReflectionFunction($functionName, $namespaceLevelNode); |
349
|
|
|
} |
350
|
|
|
} |
351
|
|
|
|
352
|
17 |
|
return $functions; |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* Searches for constants in the given AST |
357
|
|
|
* |
358
|
|
|
* @param bool $withDefined Include constants defined via "define(...)" in results. |
359
|
|
|
* |
360
|
|
|
* @return array |
361
|
|
|
*/ |
362
|
28 |
|
private function findConstants(bool $withDefined = false): array |
363
|
|
|
{ |
364
|
28 |
|
$constants = []; |
365
|
28 |
|
$expressionSolver = new NodeExpressionResolver($this); |
366
|
|
|
|
367
|
|
|
// constants can be only top-level nodes in the namespace, so we can scan them directly |
368
|
28 |
|
foreach ($this->namespaceNode->stmts as $namespaceLevelNode) { |
369
|
28 |
|
if ($namespaceLevelNode instanceof Const_) { |
370
|
26 |
|
$nodeConstants = $namespaceLevelNode->consts; |
371
|
26 |
|
if (!empty($nodeConstants)) { |
372
|
26 |
|
foreach ($nodeConstants as $nodeConstant) { |
373
|
26 |
|
$expressionSolver->process($nodeConstant->value); |
374
|
26 |
|
$constants[$nodeConstant->name->toString()] = $expressionSolver->getValue(); |
375
|
|
|
} |
376
|
|
|
} |
377
|
|
|
} |
378
|
|
|
} |
379
|
|
|
|
380
|
28 |
|
if ($withDefined) { |
381
|
2 |
|
foreach ($this->namespaceNode->stmts as $namespaceLevelNode) { |
382
|
2 |
|
if ($namespaceLevelNode instanceof Expression |
383
|
2 |
|
&& $namespaceLevelNode->expr instanceof FuncCall |
384
|
2 |
|
&& $namespaceLevelNode->expr->name instanceof Name |
385
|
2 |
|
&& (string)$namespaceLevelNode->expr->name === 'define' |
386
|
|
|
) { |
387
|
2 |
|
$functionCallNode = $namespaceLevelNode->expr; |
388
|
2 |
|
$expressionSolver->process($functionCallNode->args[0]->value); |
389
|
2 |
|
$constantName = $expressionSolver->getValue(); |
390
|
|
|
|
391
|
|
|
// Ignore constants, for which name can't be determined. |
392
|
2 |
|
if (!empty($constantName)) { |
393
|
2 |
|
$expressionSolver->process($functionCallNode->args[1]->value); |
394
|
2 |
|
$constantValue = $expressionSolver->getValue(); |
395
|
|
|
|
396
|
2 |
|
$constants[$constantName] = $constantValue; |
397
|
|
|
} |
398
|
|
|
} |
399
|
|
|
} |
400
|
|
|
} |
401
|
|
|
|
402
|
28 |
|
return $constants; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* Searches for namespace aliases for the current block |
407
|
|
|
*/ |
408
|
1 |
|
private function findNamespaceAliases(): array |
409
|
|
|
{ |
410
|
1 |
|
$namespaceAliases = []; |
411
|
|
|
|
412
|
|
|
// aliases can be only top-level nodes in the namespace, so we can scan them directly |
413
|
1 |
|
foreach ($this->namespaceNode->stmts as $namespaceLevelNode) { |
414
|
1 |
|
if ($namespaceLevelNode instanceof Use_) { |
415
|
1 |
|
$useAliases = $namespaceLevelNode->uses; |
416
|
1 |
|
if (!empty($useAliases)) { |
417
|
1 |
|
foreach ($useAliases as $useNode) { |
418
|
1 |
|
$namespaceAliases[$useNode->name->toString()] = (string) $useNode->getAlias(); |
419
|
|
|
} |
420
|
|
|
} |
421
|
|
|
} |
422
|
|
|
} |
423
|
|
|
|
424
|
1 |
|
return $namespaceAliases; |
425
|
|
|
} |
426
|
|
|
} |
427
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.