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