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