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