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