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: public function getNamespaceName(): ?string
182: {
183: return $this->namespace;
184: }
185:
186: /**
187: * Decide if this function is part of a namespace. Returns false if the class
188: * is in the global namespace or does not have a specified namespace.
189: */
190: public function inNamespace(): bool
191: {
192: return $this->namespace !== null;
193: }
194:
195: /**
196: * Get the number of parameters for this class.
197: *
198: * @return positive-int|0
199: */
200: public function getNumberOfParameters(): int
201: {
202: return count($this->parameters);
203: }
204:
205: /**
206: * Get the number of required parameters for this method.
207: *
208: * @return positive-int|0
209: */
210: public function getNumberOfRequiredParameters(): int
211: {
212: return count(array_filter($this->parameters, static function (ReflectionParameter $p) : bool {
213: return ! $p->isOptional();
214: }));
215: }
216:
217: /**
218: * Get an array list of the parameters for this method signature, as an
219: * array of ReflectionParameter instances.
220: *
221: * @return list<ReflectionParameter>
222: */
223: public function getParameters(): array
224: {
225: return array_values($this->parameters);
226: }
227:
228: /** @param list<Node\Param> $parameterNodes */
229: private function isParameterOptional(array $parameterNodes, int $parameterIndex): bool
230: {
231: foreach ($parameterNodes as $otherParameterIndex => $otherParameterNode) {
232: if ($otherParameterIndex < $parameterIndex) {
233: continue;
234: }
235:
236: // When we find next parameter that does not have a default or is not variadic,
237: // it means current parameter cannot be optional EVEN if it has a default value
238: if ($otherParameterNode->default === null && ! $otherParameterNode->variadic) {
239: return false;
240: }
241: }
242:
243: return true;
244: }
245:
246: /**
247: * Get a single parameter by name. Returns null if parameter not found for
248: * the function.
249: *
250: * @param non-empty-string $parameterName
251: */
252: public function getParameter(string $parameterName): ?\PHPStan\BetterReflection\Reflection\ReflectionParameter
253: {
254: return $this->parameters[$parameterName] ?? null;
255: }
256:
257: /** @return non-empty-string|null */
258: public function getDocComment(): ?string
259: {
260: return $this->docComment;
261: }
262:
263: /** @return non-empty-string|null */
264: public function getFileName(): ?string
265: {
266: return $this->locatedSource->getFileName();
267: }
268:
269: public function getLocatedSource(): LocatedSource
270: {
271: return $this->locatedSource;
272: }
273:
274: /**
275: * Is this function a closure?
276: */
277: public function isClosure(): bool
278: {
279: return $this->isClosure;
280: }
281:
282: public function isDeprecated(): bool
283: {
284: return AnnotationHelper::isDeprecated($this->docComment);
285: }
286:
287: public function isInternal(): bool
288: {
289: return $this->locatedSource->isInternal();
290: }
291:
292: /**
293: * Is this a user-defined function (will always return the opposite of
294: * whatever isInternal returns).
295: */
296: public function isUserDefined(): bool
297: {
298: return ! $this->isInternal();
299: }
300:
301: /** @return non-empty-string|null */
302: public function getExtensionName(): ?string
303: {
304: return $this->locatedSource->getExtensionName();
305: }
306:
307: /**
308: * Check if the function has a variadic parameter.
309: */
310: public function isVariadic(): bool
311: {
312: foreach ($this->parameters as $parameter) {
313: if ($parameter->isVariadic()) {
314: return true;
315: }
316: }
317:
318: return false;
319: }
320:
321: /** Checks if the function/method contains `throw` expressions. */
322: public function couldThrow(): bool
323: {
324: return $this->couldThrow;
325: }
326:
327: /**
328: * @param MethodNode|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $node
329: */
330: private function computeCouldThrow($node): bool
331: {
332: $statements = $node->getStmts();
333:
334: if ($statements === null) {
335: return false;
336: }
337:
338: $visitor = new FindingVisitor(static function (Node $node) : bool {
339: return $node instanceof NodeThrow;
340: });
341: $traverser = new NodeTraverser();
342: $traverser->addVisitor($visitor);
343: $traverser->traverse($statements);
344:
345: return $visitor->getFoundNodes() !== [];
346: }
347:
348: /**
349: * Recursively search an array of statements (PhpParser nodes) to find if a
350: * yield expression exists anywhere (thus indicating this is a generator).
351: */
352: private function nodeIsOrContainsYield(Node $node): bool
353: {
354: if ($node instanceof YieldNode) {
355: return true;
356: }
357:
358: if ($node instanceof YieldFromNode) {
359: return true;
360: }
361:
362: /** @psalm-var string $nodeName */
363: foreach ($node->getSubNodeNames() as $nodeName) {
364: $nodeProperty = $node->$nodeName;
365:
366: if ($nodeProperty instanceof Node && $this->nodeIsOrContainsYield($nodeProperty)) {
367: return true;
368: }
369:
370: if (! is_array($nodeProperty)) {
371: continue;
372: }
373:
374: /** @psalm-var mixed $nodePropertyArrayItem */
375: foreach ($nodeProperty as $nodePropertyArrayItem) {
376: if ($nodePropertyArrayItem instanceof Node && $this->nodeIsOrContainsYield($nodePropertyArrayItem)) {
377: return true;
378: }
379: }
380: }
381:
382: return false;
383: }
384:
385: /**
386: * Check if this function can be used as a generator (i.e. contains the
387: * "yield" keyword).
388: */
389: public function isGenerator(): bool
390: {
391: return $this->isGenerator;
392: }
393:
394: /**
395: * Get the line number that this function starts on.
396: *
397: * @return positive-int
398: *
399: * @throws CodeLocationMissing
400: */
401: public function getStartLine(): int
402: {
403: if ($this->startLine === null) {
404: throw CodeLocationMissing::create();
405: }
406:
407: return $this->startLine;
408: }
409:
410: /**
411: * Get the line number that this function ends on.
412: *
413: * @return positive-int
414: *
415: * @throws CodeLocationMissing
416: */
417: public function getEndLine(): int
418: {
419: if ($this->endLine === null) {
420: throw CodeLocationMissing::create();
421: }
422:
423: return $this->endLine;
424: }
425:
426: /**
427: * @return positive-int
428: *
429: * @throws CodeLocationMissing
430: */
431: public function getStartColumn(): int
432: {
433: if ($this->startColumn === null) {
434: throw CodeLocationMissing::create();
435: }
436:
437: return $this->startColumn;
438: }
439:
440: /**
441: * @return positive-int
442: *
443: * @throws CodeLocationMissing
444: */
445: public function getEndColumn(): int
446: {
447: if ($this->endColumn === null) {
448: throw CodeLocationMissing::create();
449: }
450:
451: return $this->endColumn;
452: }
453:
454: /**
455: * Is this function declared as a reference.
456: */
457: public function returnsReference(): bool
458: {
459: return $this->returnsReference;
460: }
461:
462: /**
463: * Get the return type declaration
464: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
465: */
466: public function getReturnType()
467: {
468: if ($this->hasTentativeReturnType()) {
469: return null;
470: }
471:
472: return $this->returnType;
473: }
474:
475: /**
476: * Do we have a return type declaration
477: */
478: public function hasReturnType(): bool
479: {
480: if ($this->hasTentativeReturnType()) {
481: return false;
482: }
483:
484: return $this->returnType !== null;
485: }
486:
487: public function hasTentativeReturnType(): bool
488: {
489: if ($this->isUserDefined()) {
490: return false;
491: }
492:
493: return AnnotationHelper::hasTentativeReturnType($this->docComment);
494: }
495:
496: /**
497: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
498: */
499: public function getTentativeReturnType()
500: {
501: if (! $this->hasTentativeReturnType()) {
502: return null;
503: }
504:
505: return $this->returnType;
506: }
507:
508: /**
509: * @param MethodNode|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $node
510: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
511: */
512: private function createReturnType($node)
513: {
514: $returnType = $node->getReturnType();
515:
516: if ($returnType === null) {
517: return null;
518: }
519:
520: assert($returnType instanceof Node\Identifier || $returnType instanceof Node\Name || $returnType instanceof Node\NullableType || $returnType instanceof Node\UnionType || $returnType instanceof Node\IntersectionType);
521:
522: return ReflectionType::createFromNode($this->reflector, $this, $returnType);
523: }
524:
525: /** @return list<ReflectionAttribute> */
526: public function getAttributes(): array
527: {
528: return $this->attributes;
529: }
530:
531: /** @return list<ReflectionAttribute> */
532: public function getAttributesByName(string $name): array
533: {
534: return ReflectionAttributeHelper::filterAttributesByName($this->getAttributes(), $name);
535: }
536:
537: /**
538: * @param class-string $className
539: *
540: * @return list<ReflectionAttribute>
541: */
542: public function getAttributesByInstance(string $className): array
543: {
544: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
545: }
546: }
547: