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