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