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