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