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