1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\Reflection;
6:
7: use Closure;
8: use Exception;
9: use InvalidArgumentException;
10: use LogicException;
11: use OutOfBoundsException;
12: use PhpParser\Node;
13: use PhpParser\Node\Param as ParamNode;
14: use ReflectionClass as CoreReflectionClass;
15: use PHPStan\BetterReflection\BetterReflection;
16: use PHPStan\BetterReflection\NodeCompiler\CompiledValue;
17: use PHPStan\BetterReflection\NodeCompiler\CompileNodeToValue;
18: use PHPStan\BetterReflection\NodeCompiler\CompilerContext;
19: use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode;
20: use PHPStan\BetterReflection\Reflection\Attribute\ReflectionAttributeHelper;
21: use PHPStan\BetterReflection\Reflection\Exception\CodeLocationMissing;
22: use PHPStan\BetterReflection\Reflection\StringCast\ReflectionParameterStringCast;
23: use PHPStan\BetterReflection\Reflector\Reflector;
24: use PHPStan\BetterReflection\Util\CalculateReflectionColumn;
25: use PHPStan\BetterReflection\Util\Exception\NoNodePosition;
26:
27: use function array_map;
28: use function assert;
29: use function count;
30: use function is_array;
31: use function is_object;
32: use function is_string;
33: use function sprintf;
34:
35: /** @psalm-immutable */
36: class ReflectionParameter
37: {
38: private Reflector $reflector;
39: /**
40: * @var \PHPStan\BetterReflection\Reflection\ReflectionMethod|\PHPStan\BetterReflection\Reflection\ReflectionFunction
41: */
42: private $function;
43: private int $parameterIndex;
44: private bool $isOptional;
45: /** @var non-empty-string */
46: private string $name;
47:
48: /**
49: * @var \PhpParser\Node\Expr|null
50: */
51: private $default;
52:
53: /**
54: * @var \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
55: */
56: private $type;
57:
58: private bool $isVariadic;
59:
60: private bool $byRef;
61:
62: private bool $isPromoted;
63:
64: /** @var list<ReflectionAttribute> */
65: private array $attributes;
66:
67: /** @var positive-int|null */
68: private $startLine;
69:
70: /** @var positive-int|null */
71: private $endLine;
72:
73: /** @var positive-int|null */
74: private $startColumn;
75:
76: /** @var positive-int|null */
77: private $endColumn;
78:
79: /** @psalm-allow-private-mutation
80: * @var \PHPStan\BetterReflection\NodeCompiler\CompiledValue|null */
81: private $compiledDefaultValue = null;
82:
83: /**
84: * @param \PHPStan\BetterReflection\Reflection\ReflectionMethod|\PHPStan\BetterReflection\Reflection\ReflectionFunction $function
85: */
86: private function __construct(Reflector $reflector, ParamNode $node, $function, int $parameterIndex, bool $isOptional)
87: {
88: $this->reflector = $reflector;
89: $this->function = $function;
90: $this->parameterIndex = $parameterIndex;
91: $this->isOptional = $isOptional;
92: assert($node->var instanceof Node\Expr\Variable);
93: assert(is_string($node->var->name));
94: $name = $node->var->name;
95: assert($name !== '');
96: $this->name = $name;
97: $this->default = $node->default;
98: $this->isPromoted = $node->flags !== 0;
99: $this->type = $this->createType($node);
100: $this->isVariadic = $node->variadic;
101: $this->byRef = $node->byRef;
102: $this->attributes = ReflectionAttributeHelper::createAttributes($reflector, $this, $node->attrGroups);
103: $startLine = $node->getStartLine();
104: if ($startLine === -1) {
105: $startLine = null;
106: }
107: $endLine = $node->getEndLine();
108: if ($endLine === -1) {
109: $endLine = null;
110: }
111: /** @psalm-suppress InvalidPropertyAssignmentValue */
112: $this->startLine = $startLine;
113: /** @psalm-suppress InvalidPropertyAssignmentValue */
114: $this->endLine = $endLine;
115: try {
116: $this->startColumn = CalculateReflectionColumn::getStartColumn($function->getLocatedSource()->getSource(), $node);
117: } catch (NoNodePosition $exception) {
118: $this->startColumn = null;
119: }
120: try {
121: $this->endColumn = CalculateReflectionColumn::getEndColumn($function->getLocatedSource()->getSource(), $node);
122: } catch (NoNodePosition $exception) {
123: $this->endColumn = null;
124: }
125: }
126:
127: /**
128: * @return array<string, mixed>
129: */
130: public function exportToCache(): array
131: {
132: $br = new BetterReflection();
133:
134: return [
135: 'name' => $this->name,
136: 'default' => $this->default !== null ? $br->printer()->prettyPrintExpr($this->default) : null,
137: 'type' => $this->type !== null ? ['class' => get_class($this->type), 'data' => $this->type->exportToCache()] : null,
138: 'isVariadic' => $this->isVariadic,
139: 'byRef' => $this->byRef,
140: 'isPromoted' => $this->isPromoted,
141: 'attributes' => array_map(
142: static fn (ReflectionAttribute $attr) => $attr->exportToCache(),
143: $this->attributes,
144: ),
145: 'startLine' => $this->startLine,
146: 'endLine' => $this->endLine,
147: 'startColumn' => $this->startColumn,
148: 'endColumn' => $this->endColumn,
149: 'parameterIndex' => $this->parameterIndex,
150: 'isOptional' => $this->isOptional,
151: ];
152: }
153:
154: /**
155: * @param array<string, mixed> $data
156: * @param \PHPStan\BetterReflection\Reflection\ReflectionMethod|\PHPStan\BetterReflection\Reflection\ReflectionFunction $function
157: */
158: public static function importFromCache(Reflector $reflector, array $data, $function): self
159: {
160: $reflection = new CoreReflectionClass(self::class);
161: /** @var self $ref */
162: $ref = $reflection->newInstanceWithoutConstructor();
163: $ref->reflector = $reflector;
164: $ref->function = $function;
165: $ref->parameterIndex = $data['parameterIndex'];
166: $ref->isOptional = $data['isOptional'];
167: $ref->name = $data['name'];
168:
169: if ($data['default'] !== null) {
170: $br = new BetterReflection();
171: $ref->default = $br->phpParser()->parse('<?php ' . $data['default'] . ';')[0]->expr;
172: } else {
173: $ref->default = null;
174: }
175:
176: if ($data['type'] !== null) {
177: $typeClass = $data['type']['class'];
178: $ref->type = $typeClass::importFromCache($reflector, $data['type']['data'], $ref);
179: } else {
180: $ref->type = null;
181: }
182:
183: $ref->isVariadic = $data['isVariadic'];
184: $ref->byRef = $data['byRef'];
185: $ref->isPromoted = $data['isPromoted'];
186: $ref->attributes = array_map(
187: static fn ($attrData) => ReflectionAttribute::importFromCache($reflector, $attrData, $ref),
188: $data['attributes'],
189: );
190: $ref->startLine = $data['startLine'];
191: $ref->endLine = $data['endLine'];
192: $ref->startColumn = $data['startColumn'];
193: $ref->endColumn = $data['endColumn'];
194:
195: return $ref;
196: }
197:
198: /**
199: * Create a reflection of a parameter using an instance
200: *
201: * @param non-empty-string $methodName
202: * @param non-empty-string $parameterName
203: *
204: * @throws OutOfBoundsException
205: */
206: public static function createFromClassInstanceAndMethod(object $instance, string $methodName, string $parameterName): self
207: {
208: $parameter = ($nullsafeVariable1 = ReflectionClass::createFromInstance($instance)
209: ->getMethod($methodName)) ? $nullsafeVariable1->getParameter($parameterName) : null;
210: if ($parameter === null) {
211: throw new OutOfBoundsException(sprintf('Could not find parameter: %s', $parameterName));
212: }
213: return $parameter;
214: }
215:
216: /**
217: * Create a reflection of a parameter using a closure
218: *
219: * @param non-empty-string $parameterName
220: *
221: * @throws OutOfBoundsException
222: */
223: public static function createFromClosure(Closure $closure, string $parameterName): ReflectionParameter
224: {
225: $parameter = ReflectionFunction::createFromClosure($closure)
226: ->getParameter($parameterName);
227:
228: if ($parameter === null) {
229: throw new OutOfBoundsException(sprintf('Could not find parameter: %s', $parameterName));
230: }
231:
232: return $parameter;
233: }
234:
235: /** @return non-empty-string */
236: public function __toString(): string
237: {
238: return ReflectionParameterStringCast::toString($this);
239: }
240:
241: /**
242: * @internal
243: *
244: * @param ParamNode $node Node has to be processed by the PhpParser\NodeVisitor\NameResolver
245: * @param \PHPStan\BetterReflection\Reflection\ReflectionMethod|\PHPStan\BetterReflection\Reflection\ReflectionFunction $function
246: */
247: public static function createFromNode(Reflector $reflector, ParamNode $node, $function, int $parameterIndex, bool $isOptional): self
248: {
249: return new self(
250: $reflector,
251: $node,
252: $function,
253: $parameterIndex,
254: $isOptional,
255: );
256: }
257:
258: /** @internal
259: * @param \PHPStan\BetterReflection\Reflection\ReflectionMethod|\PHPStan\BetterReflection\Reflection\ReflectionFunction $function */
260: public function withFunction($function): self
261: {
262: $clone = clone $this;
263: $clone->function = $function;
264:
265: if ($clone->type !== null) {
266: $clone->type = $clone->type->withOwner($clone);
267: }
268:
269: $clone->attributes = array_map(static fn (ReflectionAttribute $attribute): ReflectionAttribute => $attribute->withOwner($clone), $this->attributes);
270:
271: $this->compiledDefaultValue = null;
272:
273: return $clone;
274: }
275:
276: /** @throws LogicException */
277: private function getCompiledDefaultValue(): CompiledValue
278: {
279: if (! $this->isDefaultValueAvailable()) {
280: throw new LogicException('This parameter does not have a default value available');
281: }
282:
283: if ($this->compiledDefaultValue === null) {
284: $this->compiledDefaultValue = (new CompileNodeToValue())->__invoke(
285: $this->default,
286: new CompilerContext($this->reflector, $this),
287: );
288: }
289:
290: return $this->compiledDefaultValue;
291: }
292:
293: /**
294: * Get the name of the parameter.
295: *
296: * @return non-empty-string
297: */
298: public function getName(): string
299: {
300: return $this->name;
301: }
302:
303: /**
304: * Get the function (or method) that declared this parameter.
305: * @return \PHPStan\BetterReflection\Reflection\ReflectionMethod|\PHPStan\BetterReflection\Reflection\ReflectionFunction
306: */
307: public function getDeclaringFunction()
308: {
309: return $this->function;
310: }
311:
312: /**
313: * Get the class from the method that this parameter belongs to, if it
314: * exists.
315: *
316: * This will return null if the declaring function is not a method.
317: */
318: public function getDeclaringClass(): ?\PHPStan\BetterReflection\Reflection\ReflectionClass
319: {
320: if ($this->function instanceof ReflectionMethod) {
321: return $this->function->getDeclaringClass();
322: }
323:
324: return null;
325: }
326:
327: public function getImplementingClass(): ?\PHPStan\BetterReflection\Reflection\ReflectionClass
328: {
329: if ($this->function instanceof ReflectionMethod) {
330: return $this->function->getImplementingClass();
331: }
332:
333: return null;
334: }
335:
336: /**
337: * Is the parameter optional?
338: *
339: * Note this is distinct from "isDefaultValueAvailable" because you can have
340: * a default value, but the parameter not be optional. In the example, the
341: * $foo parameter isOptional() == false, but isDefaultValueAvailable == true
342: *
343: * @example someMethod($foo = 'foo', $bar)
344: */
345: public function isOptional(): bool
346: {
347: return $this->isOptional;
348: }
349:
350: /**
351: * Does the parameter have a default, regardless of whether it is optional.
352: *
353: * Note this is distinct from "isOptional" because you can have
354: * a default value, but the parameter not be optional. In the example, the
355: * $foo parameter isOptional() == false, but isDefaultValueAvailable == true
356: *
357: * @example someMethod($foo = 'foo', $bar)
358: * @psalm-assert-if-true Node\Expr $this->default
359: */
360: public function isDefaultValueAvailable(): bool
361: {
362: return $this->default !== null;
363: }
364:
365: public function getDefaultValueExpression(): ?\PhpParser\Node\Expr
366: {
367: return $this->default;
368: }
369:
370: /**
371: * Get the default value of the parameter.
372: *
373: * @throws LogicException
374: * @throws UnableToCompileNode
375: * @return mixed
376: */
377: public function getDefaultValue()
378: {
379: /** @psalm-var scalar|array<scalar>|null $value */
380: $value = $this->getCompiledDefaultValue()->value;
381:
382: return $value;
383: }
384:
385: /**
386: * Does this method allow null for a parameter?
387: */
388: public function allowsNull(): bool
389: {
390: $type = $this->getType();
391:
392: if ($type === null) {
393: return true;
394: }
395:
396: return $type->allowsNull();
397: }
398:
399: /**
400: * Find the position of the parameter, left to right, starting at zero.
401: */
402: public function getPosition(): int
403: {
404: return $this->parameterIndex;
405: }
406:
407: /**
408: * Get the ReflectionType instance representing the type declaration for
409: * this parameter
410: *
411: * (note: this has nothing to do with DocBlocks).
412: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
413: */
414: public function getType()
415: {
416: return $this->type;
417: }
418:
419: /**
420: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
421: */
422: private function createType(ParamNode $node)
423: {
424: $type = $node->type;
425:
426: if ($type === null) {
427: return null;
428: }
429:
430: assert($type instanceof Node\Identifier || $type instanceof Node\Name || $type instanceof Node\NullableType || $type instanceof Node\UnionType || $type instanceof Node\IntersectionType);
431:
432: $allowsNull = $this->default instanceof Node\Expr\ConstFetch && $this->default->name->toLowerString() === 'null' && ! $this->isPromoted;
433:
434: return ReflectionType::createFromNode($this->reflector, $this, $type, $allowsNull);
435: }
436:
437: /**
438: * Does this parameter have a type declaration?
439: *
440: * (note: this has nothing to do with DocBlocks).
441: */
442: public function hasType(): bool
443: {
444: return $this->type !== null;
445: }
446:
447: /**
448: * Is this parameter a variadic (denoted by ...$param).
449: */
450: public function isVariadic(): bool
451: {
452: return $this->isVariadic;
453: }
454:
455: /**
456: * Is this parameter passed by reference (denoted by &$param).
457: */
458: public function isPassedByReference(): bool
459: {
460: return $this->byRef;
461: }
462:
463: public function canBePassedByValue(): bool
464: {
465: return ! $this->isPassedByReference();
466: }
467:
468: public function isPromoted(): bool
469: {
470: return $this->isPromoted;
471: }
472:
473: /** @throws LogicException */
474: public function isDefaultValueConstant(): bool
475: {
476: return $this->getCompiledDefaultValue()->constantName !== null;
477: }
478:
479: /** @throws LogicException */
480: public function getDefaultValueConstantName(): string
481: {
482: $compiledDefaultValue = $this->getCompiledDefaultValue();
483:
484: if ($compiledDefaultValue->constantName === null) {
485: throw new LogicException('This parameter is not a constant default value, so cannot have a constant name');
486: }
487:
488: return $compiledDefaultValue->constantName;
489: }
490:
491: /**
492: * @return positive-int
493: *
494: * @throws CodeLocationMissing
495: */
496: public function getStartLine(): int
497: {
498: if ($this->startLine === null) {
499: throw CodeLocationMissing::create(sprintf('Was looking for parameter "$%s".', $this->name));
500: }
501:
502: return $this->startLine;
503: }
504:
505: /**
506: * @return positive-int
507: *
508: * @throws CodeLocationMissing
509: */
510: public function getEndLine(): int
511: {
512: if ($this->endLine === null) {
513: throw CodeLocationMissing::create(sprintf('Was looking for parameter "$%s".', $this->name));
514: }
515:
516: return $this->endLine;
517: }
518:
519: /**
520: * @return positive-int
521: *
522: * @throws CodeLocationMissing
523: */
524: public function getStartColumn(): int
525: {
526: if ($this->startColumn === null) {
527: throw CodeLocationMissing::create(sprintf('Was looking for parameter "$%s".', $this->name));
528: }
529:
530: return $this->startColumn;
531: }
532:
533: /**
534: * @return positive-int
535: *
536: * @throws CodeLocationMissing
537: */
538: public function getEndColumn(): int
539: {
540: if ($this->endColumn === null) {
541: throw CodeLocationMissing::create(sprintf('Was looking for parameter "$%s".', $this->name));
542: }
543:
544: return $this->endColumn;
545: }
546:
547: /** @return list<ReflectionAttribute> */
548: public function getAttributes(): array
549: {
550: return $this->attributes;
551: }
552:
553: /** @return list<ReflectionAttribute> */
554: public function getAttributesByName(string $name): array
555: {
556: return ReflectionAttributeHelper::filterAttributesByName($this->getAttributes(), $name);
557: }
558:
559: /**
560: * @param class-string $className
561: *
562: * @return list<ReflectionAttribute>
563: */
564: public function getAttributesByInstance(string $className): array
565: {
566: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
567: }
568: }
569: