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