1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\Reflection;
6:
7: use PhpParser\Node;
8: use PhpParser\Node\Expr\Throw_ as NodeThrow;
9: use PhpParser\Node\Expr\Yield_ as YieldNode;
10: use PhpParser\Node\Expr\YieldFrom as YieldFromNode;
11: use PhpParser\Node\Stmt\ClassMethod as MethodNode;
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 = $node->getStartLine();
116: if ($startLine === -1) {
117: $startLine = null;
118: }
119:
120: $endLine = $node->getEndLine();
121: if ($endLine === -1) {
122: $endLine = null;
123: }
124:
125: /** @psalm-suppress InvalidPropertyAssignmentValue */
126: $this->startLine = $startLine;
127: /** @psalm-suppress InvalidPropertyAssignmentValue */
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($visitor);
344: $traverser->traverse($statements);
345:
346: return $visitor->getFoundNodes() !== [];
347: }
348:
349: /**
350: * Recursively search an array of statements (PhpParser nodes) to find if a
351: * yield expression exists anywhere (thus indicating this is a generator).
352: */
353: private function nodeIsOrContainsYield(Node $node): bool
354: {
355: if ($node instanceof YieldNode) {
356: return true;
357: }
358:
359: if ($node instanceof YieldFromNode) {
360: return true;
361: }
362:
363: /** @psalm-var string $nodeName */
364: foreach ($node->getSubNodeNames() as $nodeName) {
365: $nodeProperty = $node->$nodeName;
366:
367: if ($nodeProperty instanceof Node && $this->nodeIsOrContainsYield($nodeProperty)) {
368: return true;
369: }
370:
371: if (! is_array($nodeProperty)) {
372: continue;
373: }
374:
375: /** @psalm-var mixed $nodePropertyArrayItem */
376: foreach ($nodeProperty as $nodePropertyArrayItem) {
377: if ($nodePropertyArrayItem instanceof Node && $this->nodeIsOrContainsYield($nodePropertyArrayItem)) {
378: return true;
379: }
380: }
381: }
382:
383: return false;
384: }
385:
386: /**
387: * Check if this function can be used as a generator (i.e. contains the
388: * "yield" keyword).
389: */
390: public function isGenerator(): bool
391: {
392: return $this->isGenerator;
393: }
394:
395: /**
396: * Get the line number that this function starts on.
397: *
398: * @return positive-int
399: *
400: * @throws CodeLocationMissing
401: */
402: public function getStartLine(): int
403: {
404: if ($this->startLine === null) {
405: throw CodeLocationMissing::create();
406: }
407:
408: return $this->startLine;
409: }
410:
411: /**
412: * Get the line number that this function ends on.
413: *
414: * @return positive-int
415: *
416: * @throws CodeLocationMissing
417: */
418: public function getEndLine(): int
419: {
420: if ($this->endLine === null) {
421: throw CodeLocationMissing::create();
422: }
423:
424: return $this->endLine;
425: }
426:
427: /**
428: * @return positive-int
429: *
430: * @throws CodeLocationMissing
431: */
432: public function getStartColumn(): int
433: {
434: if ($this->startColumn === null) {
435: throw CodeLocationMissing::create();
436: }
437:
438: return $this->startColumn;
439: }
440:
441: /**
442: * @return positive-int
443: *
444: * @throws CodeLocationMissing
445: */
446: public function getEndColumn(): int
447: {
448: if ($this->endColumn === null) {
449: throw CodeLocationMissing::create();
450: }
451:
452: return $this->endColumn;
453: }
454:
455: /**
456: * Is this function declared as a reference.
457: */
458: public function returnsReference(): bool
459: {
460: return $this->returnsReference;
461: }
462:
463: /**
464: * Get the return type declaration
465: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
466: */
467: public function getReturnType()
468: {
469: if ($this->hasTentativeReturnType()) {
470: return null;
471: }
472:
473: return $this->returnType;
474: }
475:
476: /**
477: * Do we have a return type declaration
478: */
479: public function hasReturnType(): bool
480: {
481: if ($this->hasTentativeReturnType()) {
482: return false;
483: }
484:
485: return $this->returnType !== null;
486: }
487:
488: public function hasTentativeReturnType(): bool
489: {
490: if ($this->isUserDefined()) {
491: return false;
492: }
493:
494: return AnnotationHelper::hasTentativeReturnType($this->docComment);
495: }
496:
497: /**
498: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
499: */
500: public function getTentativeReturnType()
501: {
502: if (! $this->hasTentativeReturnType()) {
503: return null;
504: }
505:
506: return $this->returnType;
507: }
508:
509: /**
510: * @param MethodNode|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $node
511: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
512: */
513: private function createReturnType($node)
514: {
515: $returnType = $node->getReturnType();
516:
517: if ($returnType === null) {
518: return null;
519: }
520:
521: assert($returnType instanceof Node\Identifier || $returnType instanceof Node\Name || $returnType instanceof Node\NullableType || $returnType instanceof Node\UnionType || $returnType instanceof Node\IntersectionType);
522:
523: return ReflectionType::createFromNode($this->reflector, $this, $returnType);
524: }
525:
526: /** @return list<ReflectionAttribute> */
527: public function getAttributes(): array
528: {
529: return $this->attributes;
530: }
531:
532: /** @return list<ReflectionAttribute> */
533: public function getAttributesByName(string $name): array
534: {
535: return ReflectionAttributeHelper::filterAttributesByName($this->getAttributes(), $name);
536: }
537:
538: /**
539: * @param class-string $className
540: *
541: * @return list<ReflectionAttribute>
542: */
543: public function getAttributesByInstance(string $className): array
544: {
545: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
546: }
547: }
548: