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