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