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