1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\Reflection;
6:
7: use PhpParser\Node;
8: use PhpParser\Node\Expr\Yield_ as YieldNode;
9: use PhpParser\Node\Expr\YieldFrom as YieldFromNode;
10: use PhpParser\Node\Stmt\ClassMethod as MethodNode;
11: use PhpParser\Node\Stmt\Throw_ as NodeThrow;
12: use PhpParser\NodeTraverser;
13: use PhpParser\NodeVisitor\FindingVisitor;
14: use PHPStan\BetterReflection\Reflection\Annotation\AnnotationHelper;
15: use PHPStan\BetterReflection\Reflection\Attribute\ReflectionAttributeHelper;
16: use PHPStan\BetterReflection\Reflection\Exception\CodeLocationMissing;
17: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
18: use PHPStan\BetterReflection\Util\CalculateReflectionColumn;
19: use PHPStan\BetterReflection\Util\Exception\NoNodePosition;
20: use PHPStan\BetterReflection\Util\GetLastDocComment;
21:
22: use function array_filter;
23: use function array_values;
24: use function assert;
25: use function count;
26: use function is_array;
27:
28: /** @psalm-immutable */
29: trait ReflectionFunctionAbstract
30: {
31: /**
32: * @var non-empty-string
33: * @psalm-allow-private-mutation
34: */
35: private $name;
36:
37: /**
38: * @var array<non-empty-string, ReflectionParameter>
39: * @psalm-allow-private-mutation
40: */
41: private $parameters;
42:
43: /** @psalm-allow-private-mutation
44: * @var bool */
45: private $returnsReference;
46:
47: /** @psalm-allow-private-mutation
48: * @var \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null */
49: private $returnType;
50:
51: /**
52: * @var list<ReflectionAttribute>
53: * @psalm-allow-private-mutation
54: */
55: private $attributes;
56:
57: /**
58: * @var non-empty-string|null
59: * @psalm-allow-private-mutation
60: */
61: private $docComment;
62:
63: /**
64: * @var positive-int|null
65: * @psalm-allow-private-mutation
66: */
67: private $startLine;
68:
69: /**
70: * @var positive-int|null
71: * @psalm-allow-private-mutation
72: */
73: private $endLine;
74:
75: /**
76: * @var positive-int|null
77: * @psalm-allow-private-mutation
78: */
79: private $startColumn;
80:
81: /**
82: * @var positive-int|null
83: * @psalm-allow-private-mutation
84: */
85: private $endColumn;
86:
87: /** @psalm-allow-private-mutation
88: * @var bool */
89: private $couldThrow = false;
90:
91: /** @psalm-allow-private-mutation
92: * @var bool */
93: private $isClosure = false;
94: /** @psalm-allow-private-mutation
95: * @var bool */
96: private $isGenerator = false;
97:
98: /** @return non-empty-string */
99: abstract public function __toString(): string;
100:
101: /** @return non-empty-string */
102: abstract public function getShortName(): string;
103:
104: /** @psalm-external-mutation-free
105: * @param MethodNode|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $node */
106: private function fillFromNode($node): void
107: {
108: $this->parameters = $this->createParameters($node);
109: $this->returnsReference = $node->returnsByRef();
110: $this->returnType = $this->createReturnType($node);
111: $this->attributes = ReflectionAttributeHelper::createAttributes($this->reflector, $this, $node->attrGroups);
112: $this->docComment = GetLastDocComment::forNode($node);
113: $this->couldThrow = $this->computeCouldThrow($node);
114:
115: $startLine = null;
116: if ($node->hasAttribute('startLine')) {
117: $startLine = $node->getStartLine();
118: assert($startLine > 0);
119: }
120:
121: $endLine = null;
122: if ($node->hasAttribute('endLine')) {
123: $endLine = $node->getEndLine();
124: assert($endLine > 0);
125: }
126:
127: $this->startLine = $startLine;
128: $this->endLine = $endLine;
129:
130: try {
131: $this->startColumn = CalculateReflectionColumn::getStartColumn($this->getLocatedSource()->getSource(), $node);
132: } catch (NoNodePosition $exception) {
133: $this->startColumn = null;
134: }
135:
136: try {
137: $this->endColumn = CalculateReflectionColumn::getEndColumn($this->getLocatedSource()->getSource(), $node);
138: } catch (NoNodePosition $exception) {
139: $this->endColumn = null;
140: }
141: }
142:
143: /** @return array<non-empty-string, ReflectionParameter>
144: * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $node */
145: private function createParameters($node): array
146: {
147: $parameters = [];
148:
149: /** @var list<Node\Param> $nodeParams */
150: $nodeParams = $node->params;
151: foreach ($nodeParams as $paramIndex => $paramNode) {
152: $parameter = ReflectionParameter::createFromNode($this->reflector, $paramNode, $this, $paramIndex, $this->isParameterOptional($nodeParams, $paramIndex));
153:
154: $parameters[$parameter->getName()] = $parameter;
155: }
156:
157: return $parameters;
158: }
159:
160: /**
161: * Get the "full" name of the function (e.g. for A\B\foo, this will return
162: * "A\B\foo").
163: *
164: * @return non-empty-string
165: */
166: public function getName(): string
167: {
168: $namespace = $this->getNamespaceName();
169:
170: if ($namespace === null) {
171: return $this->getShortName();
172: }
173:
174: return $namespace . '\\' . $this->getShortName();
175: }
176:
177: /**
178: * Get the "namespace" name of the function (e.g. for A\B\foo, this will
179: * return "A\B").
180: *
181: * @return non-empty-string|null
182: */
183: public function getNamespaceName(): ?string
184: {
185: return $this->namespace;
186: }
187:
188: /**
189: * Decide if this function is part of a namespace. Returns false if the class
190: * is in the global namespace or does not have a specified namespace.
191: */
192: public function inNamespace(): bool
193: {
194: return $this->namespace !== null;
195: }
196:
197: /**
198: * Get the number of parameters for this class.
199: *
200: * @return positive-int|0
201: */
202: public function getNumberOfParameters(): int
203: {
204: return count($this->parameters);
205: }
206:
207: /**
208: * Get the number of required parameters for this method.
209: *
210: * @return positive-int|0
211: */
212: public function getNumberOfRequiredParameters(): int
213: {
214: return count(array_filter($this->parameters, static function (ReflectionParameter $p) : bool {
215: return ! $p->isOptional();
216: }));
217: }
218:
219: /**
220: * Get an array list of the parameters for this method signature, as an
221: * array of ReflectionParameter instances.
222: *
223: * @return list<ReflectionParameter>
224: */
225: public function getParameters(): array
226: {
227: return array_values($this->parameters);
228: }
229:
230: /** @param list<Node\Param> $parameterNodes */
231: private function isParameterOptional(array $parameterNodes, int $parameterIndex): bool
232: {
233: foreach ($parameterNodes as $otherParameterIndex => $otherParameterNode) {
234: if ($otherParameterIndex < $parameterIndex) {
235: continue;
236: }
237:
238: // When we find next parameter that does not have a default or is not variadic,
239: // it means current parameter cannot be optional EVEN if it has a default value
240: if ($otherParameterNode->default === null && ! $otherParameterNode->variadic) {
241: return false;
242: }
243: }
244:
245: return true;
246: }
247:
248: /**
249: * Get a single parameter by name. Returns null if parameter not found for
250: * the function.
251: *
252: * @param non-empty-string $parameterName
253: */
254: public function getParameter(string $parameterName): ?\PHPStan\BetterReflection\Reflection\ReflectionParameter
255: {
256: return $this->parameters[$parameterName] ?? null;
257: }
258:
259: /** @return non-empty-string|null */
260: public function getDocComment(): ?string
261: {
262: return $this->docComment;
263: }
264:
265: /** @return non-empty-string|null */
266: public function getFileName(): ?string
267: {
268: return $this->locatedSource->getFileName();
269: }
270:
271: public function getLocatedSource(): LocatedSource
272: {
273: return $this->locatedSource;
274: }
275:
276: /**
277: * Is this function a closure?
278: */
279: public function isClosure(): bool
280: {
281: return $this->isClosure;
282: }
283:
284: public function isDeprecated(): bool
285: {
286: return AnnotationHelper::isDeprecated($this->docComment);
287: }
288:
289: public function isInternal(): bool
290: {
291: return $this->locatedSource->isInternal();
292: }
293:
294: /**
295: * Is this a user-defined function (will always return the opposite of
296: * whatever isInternal returns).
297: */
298: public function isUserDefined(): bool
299: {
300: return ! $this->isInternal();
301: }
302:
303: /** @return non-empty-string|null */
304: public function getExtensionName(): ?string
305: {
306: return $this->locatedSource->getExtensionName();
307: }
308:
309: /**
310: * Check if the function has a variadic parameter.
311: */
312: public function isVariadic(): bool
313: {
314: foreach ($this->parameters as $parameter) {
315: if ($parameter->isVariadic()) {
316: return true;
317: }
318: }
319:
320: return false;
321: }
322:
323: /** Checks if the function/method contains `throw` expressions. */
324: public function couldThrow(): bool
325: {
326: return $this->couldThrow;
327: }
328:
329: /**
330: * @param MethodNode|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $node
331: */
332: private function computeCouldThrow($node): bool
333: {
334: $statements = $node->getStmts();
335:
336: if ($statements === null) {
337: return false;
338: }
339:
340: $visitor = new FindingVisitor(static function (Node $node) : bool {
341: return $node instanceof NodeThrow;
342: });
343: $traverser = new NodeTraverser();
344: $traverser->addVisitor($visitor);
345: $traverser->traverse($statements);
346:
347: return $visitor->getFoundNodes() !== [];
348: }
349:
350: /**
351: * Recursively search an array of statements (PhpParser nodes) to find if a
352: * yield expression exists anywhere (thus indicating this is a generator).
353: */
354: private function nodeIsOrContainsYield(Node $node): bool
355: {
356: if ($node instanceof YieldNode) {
357: return true;
358: }
359:
360: if ($node instanceof YieldFromNode) {
361: return true;
362: }
363:
364: /** @psalm-var string $nodeName */
365: foreach ($node->getSubNodeNames() as $nodeName) {
366: $nodeProperty = $node->$nodeName;
367:
368: if ($nodeProperty instanceof Node && $this->nodeIsOrContainsYield($nodeProperty)) {
369: return true;
370: }
371:
372: if (! is_array($nodeProperty)) {
373: continue;
374: }
375:
376: /** @psalm-var mixed $nodePropertyArrayItem */
377: foreach ($nodeProperty as $nodePropertyArrayItem) {
378: if ($nodePropertyArrayItem instanceof Node && $this->nodeIsOrContainsYield($nodePropertyArrayItem)) {
379: return true;
380: }
381: }
382: }
383:
384: return false;
385: }
386:
387: /**
388: * Check if this function can be used as a generator (i.e. contains the
389: * "yield" keyword).
390: */
391: public function isGenerator(): bool
392: {
393: return $this->isGenerator;
394: }
395:
396: /**
397: * Get the line number that this function starts on.
398: *
399: * @return positive-int
400: *
401: * @throws CodeLocationMissing
402: */
403: public function getStartLine(): int
404: {
405: if ($this->startLine === null) {
406: throw CodeLocationMissing::create();
407: }
408:
409: return $this->startLine;
410: }
411:
412: /**
413: * Get the line number that this function ends on.
414: *
415: * @return positive-int
416: *
417: * @throws CodeLocationMissing
418: */
419: public function getEndLine(): int
420: {
421: if ($this->endLine === null) {
422: throw CodeLocationMissing::create();
423: }
424:
425: return $this->endLine;
426: }
427:
428: /**
429: * @return positive-int
430: *
431: * @throws CodeLocationMissing
432: */
433: public function getStartColumn(): int
434: {
435: if ($this->startColumn === null) {
436: throw CodeLocationMissing::create();
437: }
438:
439: return $this->startColumn;
440: }
441:
442: /**
443: * @return positive-int
444: *
445: * @throws CodeLocationMissing
446: */
447: public function getEndColumn(): int
448: {
449: if ($this->endColumn === null) {
450: throw CodeLocationMissing::create();
451: }
452:
453: return $this->endColumn;
454: }
455:
456: /**
457: * Is this function declared as a reference.
458: */
459: public function returnsReference(): bool
460: {
461: return $this->returnsReference;
462: }
463:
464: /**
465: * Get the return type declaration
466: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
467: */
468: public function getReturnType()
469: {
470: if ($this->hasTentativeReturnType()) {
471: return null;
472: }
473:
474: return $this->returnType;
475: }
476:
477: /**
478: * Do we have a return type declaration
479: */
480: public function hasReturnType(): bool
481: {
482: if ($this->hasTentativeReturnType()) {
483: return false;
484: }
485:
486: return $this->returnType !== null;
487: }
488:
489: public function hasTentativeReturnType(): bool
490: {
491: if ($this->isUserDefined()) {
492: return false;
493: }
494:
495: return AnnotationHelper::hasTentativeReturnType($this->docComment);
496: }
497:
498: /**
499: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
500: */
501: public function getTentativeReturnType()
502: {
503: if (! $this->hasTentativeReturnType()) {
504: return null;
505: }
506:
507: return $this->returnType;
508: }
509:
510: /**
511: * @param MethodNode|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $node
512: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
513: */
514: private function createReturnType($node)
515: {
516: $returnType = $node->getReturnType();
517:
518: if ($returnType === null) {
519: return null;
520: }
521:
522: assert($returnType instanceof Node\Identifier || $returnType instanceof Node\Name || $returnType instanceof Node\NullableType || $returnType instanceof Node\UnionType || $returnType instanceof Node\IntersectionType);
523:
524: return ReflectionType::createFromNode($this->reflector, $this, $returnType);
525: }
526:
527: /** @return list<ReflectionAttribute> */
528: public function getAttributes(): array
529: {
530: return $this->attributes;
531: }
532:
533: /** @return list<ReflectionAttribute> */
534: public function getAttributesByName(string $name): array
535: {
536: return ReflectionAttributeHelper::filterAttributesByName($this->getAttributes(), $name);
537: }
538:
539: /**
540: * @param class-string $className
541: *
542: * @return list<ReflectionAttribute>
543: */
544: public function getAttributesByInstance(string $className): array
545: {
546: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
547: }
548: }
549: