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