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