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: class ReflectionParameter
34: {
35: /** @var non-empty-string */
36: private $name;
37:
38: /**
39: * @var \PhpParser\Node\Expr|null
40: */
41: private $default;
42:
43: /**
44: * @var \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
45: */
46: private $type;
47:
48: /**
49: * @var bool
50: */
51: private $isVariadic;
52:
53: /**
54: * @var bool
55: */
56: private $byRef;
57:
58: /**
59: * @var bool
60: */
61: private $isPromoted;
62:
63: /** @var list<ReflectionAttribute> */
64: private $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: /**
79: * @var \PHPStan\BetterReflection\NodeCompiler\CompiledValue|null
80: */
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->type = $this->createType($node);
114: $this->isVariadic = $node->variadic;
115: $this->byRef = $node->byRef;
116: $this->isPromoted = $node->flags !== 0;
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: public function __toString(): string
244: {
245: return ReflectionParameterStringCast::toString($this);
246: }
247:
248: /**
249: * @internal
250: *
251: * @param ParamNode $node Node has to be processed by the PhpParser\NodeVisitor\NameResolver
252: * @param \PHPStan\BetterReflection\Reflection\ReflectionMethod|\PHPStan\BetterReflection\Reflection\ReflectionFunction $function
253: */
254: public static function createFromNode(Reflector $reflector, ParamNode $node, $function, int $parameterIndex, bool $isOptional): self
255: {
256: return new self($reflector, $node, $function, $parameterIndex, $isOptional);
257: }
258:
259: /** @internal
260: * @param \PHPStan\BetterReflection\Reflection\ReflectionMethod|\PHPStan\BetterReflection\Reflection\ReflectionFunction $function */
261: public function withFunction($function): self
262: {
263: $clone = clone $this;
264: $clone->function = $function;
265:
266: if ($clone->type !== null) {
267: $clone->type = $clone->type->withOwner($clone);
268: }
269:
270: $clone->attributes = array_map(static function (ReflectionAttribute $attribute) use ($clone) : ReflectionAttribute {
271: return $attribute->withOwner($clone);
272: }, $this->attributes);
273:
274: $this->compiledDefaultValue = null;
275:
276: return $clone;
277: }
278:
279: /** @throws LogicException */
280: private function getCompiledDefaultValue(): CompiledValue
281: {
282: if (! $this->isDefaultValueAvailable()) {
283: throw new LogicException('This parameter does not have a default value available');
284: }
285:
286: if ($this->compiledDefaultValue === null) {
287: $this->compiledDefaultValue = (new CompileNodeToValue())->__invoke($this->default, new CompilerContext($this->reflector, $this));
288: }
289:
290: return $this->compiledDefaultValue;
291: }
292:
293: /**
294: * Get the name of the parameter.
295: *
296: * @return non-empty-string
297: */
298: public function getName(): string
299: {
300: return $this->name;
301: }
302:
303: /**
304: * Get the function (or method) that declared this parameter.
305: * @return \PHPStan\BetterReflection\Reflection\ReflectionMethod|\PHPStan\BetterReflection\Reflection\ReflectionFunction
306: */
307: public function getDeclaringFunction()
308: {
309: return $this->function;
310: }
311:
312: /**
313: * Get the class from the method that this parameter belongs to, if it
314: * exists.
315: *
316: * This will return null if the declaring function is not a method.
317: */
318: public function getDeclaringClass(): ?\PHPStan\BetterReflection\Reflection\ReflectionClass
319: {
320: if ($this->function instanceof ReflectionMethod) {
321: return $this->function->getDeclaringClass();
322: }
323:
324: return null;
325: }
326:
327: public function getImplementingClass(): ?\PHPStan\BetterReflection\Reflection\ReflectionClass
328: {
329: if ($this->function instanceof ReflectionMethod) {
330: return $this->function->getImplementingClass();
331: }
332:
333: return null;
334: }
335:
336: /**
337: * Is the parameter optional?
338: *
339: * Note this is distinct from "isDefaultValueAvailable" because you can have
340: * a default value, but the parameter not be optional. In the example, the
341: * $foo parameter isOptional() == false, but isDefaultValueAvailable == true
342: *
343: * @example someMethod($foo = 'foo', $bar)
344: */
345: public function isOptional(): bool
346: {
347: return $this->isOptional;
348: }
349:
350: /**
351: * Does the parameter have a default, regardless of whether it is optional.
352: *
353: * Note this is distinct from "isOptional" because you can have
354: * a default value, but the parameter not be optional. In the example, the
355: * $foo parameter isOptional() == false, but isDefaultValueAvailable == true
356: *
357: * @example someMethod($foo = 'foo', $bar)
358: * @psalm-assert-if-true Node\Expr $this->default
359: */
360: public function isDefaultValueAvailable(): bool
361: {
362: return $this->default !== null;
363: }
364:
365: /**
366: * @deprecated Use getDefaultValueExpression()
367: */
368: public function getDefaultValueExpr(): ?\PhpParser\Node\Expr
369: {
370: return $this->getDefaultValueExpression();
371: }
372:
373: public function getDefaultValueExpression(): ?\PhpParser\Node\Expr
374: {
375: return $this->default;
376: }
377:
378: /**
379: * Get the default value of the parameter.
380: *
381: * @deprecated Use getDefaultValueExpression()
382: *
383: * @throws LogicException
384: * @throws UnableToCompileNode
385: * @return mixed
386: */
387: public function getDefaultValue()
388: {
389: /** @psalm-var scalar|array<scalar>|null $value */
390: $value = $this->getCompiledDefaultValue()->value;
391:
392: return $value;
393: }
394:
395: /**
396: * Does this method allow null for a parameter?
397: */
398: public function allowsNull(): bool
399: {
400: $type = $this->getType();
401:
402: if ($type === null) {
403: return true;
404: }
405:
406: return $type->allowsNull();
407: }
408:
409: /**
410: * Find the position of the parameter, left to right, starting at zero.
411: */
412: public function getPosition(): int
413: {
414: return $this->parameterIndex;
415: }
416:
417: /**
418: * Get the ReflectionType instance representing the type declaration for
419: * this parameter
420: *
421: * (note: this has nothing to do with DocBlocks).
422: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
423: */
424: public function getType()
425: {
426: return $this->type;
427: }
428:
429: /**
430: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
431: */
432: private function createType(ParamNode $node)
433: {
434: $type = $node->type;
435:
436: if ($type === null) {
437: return null;
438: }
439:
440: assert($type instanceof Node\Identifier || $type instanceof Node\Name || $type instanceof Node\NullableType || $type instanceof Node\UnionType || $type instanceof Node\IntersectionType);
441:
442: $allowsNull = $this->default instanceof Node\Expr\ConstFetch && $this->default->name->toLowerString() === 'null';
443:
444: return ReflectionType::createFromNode($this->reflector, $this, $type, $allowsNull);
445: }
446:
447: /**
448: * Does this parameter have a type declaration?
449: *
450: * (note: this has nothing to do with DocBlocks).
451: */
452: public function hasType(): bool
453: {
454: return $this->type !== null;
455: }
456:
457: /**
458: * Is this parameter a variadic (denoted by ...$param).
459: */
460: public function isVariadic(): bool
461: {
462: return $this->isVariadic;
463: }
464:
465: /**
466: * Is this parameter passed by reference (denoted by &$param).
467: */
468: public function isPassedByReference(): bool
469: {
470: return $this->byRef;
471: }
472:
473: public function canBePassedByValue(): bool
474: {
475: return ! $this->isPassedByReference();
476: }
477:
478: public function isPromoted(): bool
479: {
480: return $this->isPromoted;
481: }
482:
483: /** @throws LogicException */
484: public function isDefaultValueConstant(): bool
485: {
486: return $this->getCompiledDefaultValue()->constantName !== null;
487: }
488:
489: /** @throws LogicException */
490: public function getDefaultValueConstantName(): string
491: {
492: $compiledDefaultValue = $this->getCompiledDefaultValue();
493:
494: if ($compiledDefaultValue->constantName === null) {
495: throw new LogicException('This parameter is not a constant default value, so cannot have a constant name');
496: }
497:
498: return $compiledDefaultValue->constantName;
499: }
500:
501: /**
502: * @return positive-int
503: *
504: * @throws CodeLocationMissing
505: */
506: public function getStartLine(): int
507: {
508: if ($this->startLine === null) {
509: throw CodeLocationMissing::create();
510: }
511:
512: return $this->startLine;
513: }
514:
515: /**
516: * @return positive-int
517: *
518: * @throws CodeLocationMissing
519: */
520: public function getEndLine(): int
521: {
522: if ($this->endLine === null) {
523: throw CodeLocationMissing::create();
524: }
525:
526: return $this->endLine;
527: }
528:
529: /**
530: * @return positive-int
531: *
532: * @throws CodeLocationMissing
533: */
534: public function getStartColumn(): int
535: {
536: if ($this->startColumn === null) {
537: throw CodeLocationMissing::create();
538: }
539:
540: return $this->startColumn;
541: }
542:
543: /**
544: * @return positive-int
545: *
546: * @throws CodeLocationMissing
547: */
548: public function getEndColumn(): int
549: {
550: if ($this->endColumn === null) {
551: throw CodeLocationMissing::create();
552: }
553:
554: return $this->endColumn;
555: }
556:
557: /** @return list<ReflectionAttribute> */
558: public function getAttributes(): array
559: {
560: return $this->attributes;
561: }
562:
563: /** @return list<ReflectionAttribute> */
564: public function getAttributesByName(string $name): array
565: {
566: return ReflectionAttributeHelper::filterAttributesByName($this->getAttributes(), $name);
567: }
568:
569: /**
570: * @param class-string $className
571: *
572: * @return list<ReflectionAttribute>
573: */
574: public function getAttributesByInstance(string $className): array
575: {
576: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
577: }
578: }
579: