1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\Reflection;
6:
7: use Closure;
8: use Error;
9: use OutOfBoundsException;
10: use PhpParser\Node;
11: use PhpParser\Node\Stmt\Property as PropertyNode;
12: use PhpParser\NodeTraverser;
13: use PhpParser\NodeVisitor\FindingVisitor;
14: use ReflectionClass as CoreReflectionClass;
15: use ReflectionException;
16: use ReflectionProperty as CoreReflectionProperty;
17: use PHPStan\BetterReflection\NodeCompiler\CompiledValue;
18: use PHPStan\BetterReflection\NodeCompiler\CompileNodeToValue;
19: use PHPStan\BetterReflection\NodeCompiler\CompilerContext;
20: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty as ReflectionPropertyAdapter;
21: use PHPStan\BetterReflection\Reflection\Attribute\ReflectionAttributeHelper;
22: use PHPStan\BetterReflection\Reflection\Deprecated\DeprecatedHelper;
23: use PHPStan\BetterReflection\Reflection\Exception\ClassDoesNotExist;
24: use PHPStan\BetterReflection\Reflection\Exception\CodeLocationMissing;
25: use PHPStan\BetterReflection\Reflection\Exception\NoObjectProvided;
26: use PHPStan\BetterReflection\Reflection\Exception\NotAnObject;
27: use PHPStan\BetterReflection\Reflection\Exception\ObjectNotInstanceOfClass;
28: use PHPStan\BetterReflection\Reflection\StringCast\ReflectionPropertyStringCast;
29: use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound;
30: use PHPStan\BetterReflection\Reflector\Reflector;
31: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
32: use PHPStan\BetterReflection\Util\CalculateReflectionColumn;
33: use PHPStan\BetterReflection\Util\ClassExistenceChecker;
34: use PHPStan\BetterReflection\Util\Exception\NoNodePosition;
35: use PHPStan\BetterReflection\Util\GetLastDocComment;
36:
37: use function array_map;
38: use function assert;
39: use function class_exists;
40: use function func_num_args;
41: use function get_class;
42: use function is_array;
43: use function is_object;
44: use function method_exists;
45: use function property_exists;
46: use function sprintf;
47: use function str_contains;
48: use function strcasecmp;
49:
50: /** @psalm-immutable */
51: class ReflectionProperty
52: {
53: private Reflector $reflector;
54: private bool $isPromoted;
55: private bool $declaredAtCompileTime;
56: /** @var non-empty-string */
57: private string $name;
58:
59: /** @var int-mask-of<ReflectionPropertyAdapter::IS_*> */
60: private int $modifiers;
61:
62: /**
63: * @var \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
64: */
65: private $type;
66:
67: /**
68: * The default value expression, its exported cache form (parsed into an Expr only when
69: * asked for), or null.
70: *
71: * @var Node\Expr|array<string, mixed>|null
72: */
73: private $default;
74:
75: /** @var non-empty-string|null */
76: private $docComment;
77:
78: /** @var list<ReflectionAttribute> */
79: private array $attributes;
80:
81: /** @var positive-int|null */
82: private $startLine;
83:
84: /** @var positive-int|null */
85: private $endLine;
86:
87: /** @var positive-int|null */
88: private $startColumn;
89:
90: /** @var positive-int|null */
91: private $endColumn;
92:
93: private ?ReflectionClass $declaringClass;
94:
95: private ?ReflectionClass $implementingClass;
96:
97: /** @var non-empty-string */
98: private string $declaringClassName;
99:
100: /** @var non-empty-string */
101: private string $implementingClassName;
102:
103: private bool $immediateVirtual;
104:
105: /** @var array{get?: ReflectionMethod, set?: ReflectionMethod} */
106: private array $immediateHooks;
107:
108: /**
109: * @var array{get?: ReflectionMethod, set?: ReflectionMethod}|null
110: * @psalm-allow-private-mutation
111: */
112: private $cachedHooks = null;
113:
114: /** @psalm-allow-private-mutation
115: * @var bool|null */
116: private $cachedVirtual = null;
117:
118: /** @psalm-allow-private-mutation
119: * @var \PHPStan\BetterReflection\NodeCompiler\CompiledValue|null */
120: private $compiledDefaultValue = null;
121:
122: private function __construct(
123: Reflector $reflector,
124: PropertyNode $node,
125: Node\PropertyItem $propertyNode,
126: ReflectionClass $declaringClass,
127: ReflectionClass $implementingClass,
128: bool $isPromoted,
129: bool $declaredAtCompileTime
130: ) {
131: $this->reflector = $reflector;
132: $this->isPromoted = $isPromoted;
133: $this->declaredAtCompileTime = $declaredAtCompileTime;
134: $this->declaringClass = $declaringClass;
135: $this->implementingClass = $implementingClass;
136: $this->name = $propertyNode->name->name;
137: $this->modifiers = $this->computeModifiers($node);
138: $this->type = $this->createType($node);
139: $this->default = $propertyNode->default;
140: $this->docComment = GetLastDocComment::forNode($node);
141: $this->attributes = ReflectionAttributeHelper::createAttributes($reflector, $this, $node->attrGroups);
142: $this->immediateVirtual = $this->computeImmediateVirtual($node);
143: $this->immediateHooks = $this->createImmediateHooks($node);
144:
145: $startLine = $node->getStartLine();
146: if ($startLine === -1) {
147: $startLine = null;
148: }
149:
150: $endLine = $node->getEndLine();
151: if ($endLine === -1) {
152: $endLine = null;
153: }
154:
155: /** @psalm-suppress InvalidPropertyAssignmentValue */
156: $this->startLine = $startLine;
157: /** @psalm-suppress InvalidPropertyAssignmentValue */
158: $this->endLine = $endLine;
159:
160: try {
161: $this->startColumn = CalculateReflectionColumn::getStartColumn($declaringClass->getLocatedSource()->getSource(), $node);
162: } catch (NoNodePosition $exception) {
163: $this->startColumn = null;
164: }
165:
166: try {
167: $this->endColumn = CalculateReflectionColumn::getEndColumn($declaringClass->getLocatedSource()->getSource(), $node);
168: } catch (NoNodePosition $exception) {
169: $this->endColumn = null;
170: }
171:
172: $this->declaringClassName = $this->declaringClass->getName();
173: $this->implementingClassName = $this->implementingClass->getName();
174: }
175:
176: /** @return array<string, mixed> */
177: public function exportToCache(): array
178: {
179: return [
180: 'declaringClassName' => $this->declaringClassName,
181: 'implementingClassName' => $this->implementingClassName,
182: 'name' => $this->name,
183: 'modifiers' => $this->modifiers,
184: 'type' => $this->type !== null ? ['class' => get_class($this->type), 'data' => $this->type->exportToCache()] : null,
185: 'default' => $this->default === null || is_array($this->default) ? $this->default : ExprCacheHelper::export($this->default),
186: 'docComment' => $this->docComment,
187: 'attributes' => array_map(
188: static fn (ReflectionAttribute $attr) => $attr->exportToCache(),
189: $this->attributes,
190: ),
191: 'startLine' => $this->startLine,
192: 'endLine' => $this->endLine,
193: 'startColumn' => $this->startColumn,
194: 'endColumn' => $this->endColumn,
195: 'isPromoted' => $this->isPromoted,
196: 'declaredAtCompileTime' => $this->declaredAtCompileTime,
197: 'immediateVirtual' => $this->immediateVirtual,
198: 'immediateHooks' => array_map(
199: static fn (ReflectionMethod $method) => $method->exportToCache(),
200: $this->immediateHooks,
201: ),
202: ];
203: }
204:
205: /** @param array<string, mixed> $data */
206: public static function importFromCache(Reflector $reflector, array $data, LocatedSource $locatedSource): self
207: {
208: $reflection = new CoreReflectionClass(self::class);
209: $ref = $reflection->newInstanceWithoutConstructor();
210: assert($ref instanceof self);
211: $ref->reflector = $reflector;
212: $ref->declaringClassName = $data['declaringClassName'];
213: $ref->implementingClassName = $data['implementingClassName'];
214: $ref->name = $data['name'];
215: $ref->modifiers = $data['modifiers'];
216:
217: if ($data['type'] !== null) {
218: $typeClass = $data['type']['class'];
219: $ref->type = $typeClass::importFromCache($reflector, $data['type']['data'], $ref);
220: } else {
221: $ref->type = null;
222: }
223:
224: $ref->default = $data['default'];
225:
226: $ref->docComment = $data['docComment'];
227: $ref->attributes = array_map(
228: static fn ($attrData) => ReflectionAttribute::importFromCache($reflector, $attrData, $ref),
229: $data['attributes'],
230: );
231: $ref->startLine = $data['startLine'];
232: $ref->endLine = $data['endLine'];
233: $ref->startColumn = $data['startColumn'];
234: $ref->endColumn = $data['endColumn'];
235: $ref->isPromoted = $data['isPromoted'];
236: $ref->declaredAtCompileTime = $data['declaredAtCompileTime'];
237: $ref->immediateVirtual = $data['immediateVirtual'];
238: $ref->immediateHooks = array_map(
239: static fn ($hookData) => ReflectionMethod::importFromCache($reflector, $hookData, $locatedSource, $ref),
240: $data['immediateHooks'],
241: );
242:
243: return $ref;
244: }
245:
246: /**
247: * Create a reflection of an instance's property by its name
248: *
249: * @param non-empty-string $propertyName
250: *
251: * @throws ReflectionException
252: * @throws IdentifierNotFound
253: * @throws OutOfBoundsException
254: */
255: public static function createFromInstance(object $instance, string $propertyName): self
256: {
257: $property = ReflectionClass::createFromInstance($instance)->getProperty($propertyName);
258:
259: if ($property === null) {
260: throw new OutOfBoundsException(sprintf('Could not find property: %s', $propertyName));
261: }
262:
263: return $property;
264: }
265:
266: /** @internal */
267: public function withImplementingClass(ReflectionClass $implementingClass): self
268: {
269: $clone = clone $this;
270: $clone->implementingClass = $implementingClass;
271:
272: if ($clone->type !== null) {
273: $clone->type = $clone->type->withOwner($clone);
274: }
275:
276: $clone->attributes = array_map(static fn (ReflectionAttribute $attribute): ReflectionAttribute => $attribute->withOwner($clone), $this->attributes);
277:
278: $this->compiledDefaultValue = null;
279:
280: return $clone;
281: }
282:
283: /** @return non-empty-string */
284: public function __toString(): string
285: {
286: return ReflectionPropertyStringCast::toString($this);
287: }
288:
289: /**
290: * @internal
291: *
292: * @param PropertyNode $node Node has to be processed by the PhpParser\NodeVisitor\NameResolver
293: */
294: public static function createFromNode(
295: Reflector $reflector,
296: PropertyNode $node,
297: Node\PropertyItem $propertyProperty,
298: ReflectionClass $declaringClass,
299: ReflectionClass $implementingClass,
300: bool $isPromoted = false,
301: bool $declaredAtCompileTime = true
302: ): self {
303: return new self(
304: $reflector,
305: $node,
306: $propertyProperty,
307: $declaringClass,
308: $implementingClass,
309: $isPromoted,
310: $declaredAtCompileTime,
311: );
312: }
313:
314: /**
315: * Has the property been declared at compile-time?
316: *
317: * Note that unless the property is static, this is hard coded to return
318: * true, because we are unable to reflect instances of classes, therefore
319: * we can be sure that all properties are always declared at compile-time.
320: */
321: public function isDefault(): bool
322: {
323: return $this->declaredAtCompileTime;
324: }
325:
326: public function isDynamic(): bool
327: {
328: return ! $this->isDefault();
329: }
330:
331: /**
332: * Get the core-reflection-compatible modifier values.
333: *
334: * @return int-mask-of<ReflectionPropertyAdapter::IS_*>
335: */
336: public function getModifiers(): int
337: {
338: /** @var int-mask-of<ReflectionPropertyAdapter::IS_*> $modifiers */
339: $modifiers = $this->modifiers
340: + ($this->isVirtual() ? ReflectionPropertyAdapter::IS_VIRTUAL_COMPATIBILITY : 0);
341:
342: return $modifiers;
343: }
344:
345: /**
346: * Get the name of the property.
347: *
348: * @return non-empty-string
349: */
350: public function getName(): string
351: {
352: return $this->name;
353: }
354:
355: /**
356: * Is the property private?
357: */
358: public function isPrivate(): bool
359: {
360: return (bool) ($this->modifiers & CoreReflectionProperty::IS_PRIVATE);
361: }
362:
363: public function isPrivateSet(): bool
364: {
365: return (bool) ($this->modifiers & ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY);
366: }
367:
368: /**
369: * Is the property protected?
370: */
371: public function isProtected(): bool
372: {
373: return (bool) ($this->modifiers & CoreReflectionProperty::IS_PROTECTED);
374: }
375:
376: public function isProtectedSet(): bool
377: {
378: return (bool) ($this->modifiers & ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY);
379: }
380:
381: /**
382: * Is the property public?
383: */
384: public function isPublic(): bool
385: {
386: return (bool) ($this->modifiers & CoreReflectionProperty::IS_PUBLIC);
387: }
388:
389: /**
390: * Is the property static?
391: */
392: public function isStatic(): bool
393: {
394: return (bool) ($this->modifiers & CoreReflectionProperty::IS_STATIC);
395: }
396:
397: public function isFinal(): bool
398: {
399: return (bool) ($this->modifiers & ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY);
400: }
401:
402: public function isAbstract(): bool
403: {
404: return (bool) ($this->modifiers & ReflectionPropertyAdapter::IS_ABSTRACT_COMPATIBILITY)
405: || $this->getDeclaringClass()->isInterface();
406: }
407:
408: public function isPromoted(): bool
409: {
410: return $this->isPromoted;
411: }
412:
413: public function isInitialized(?object $object = null): bool
414: {
415: if ($object === null && $this->isStatic()) {
416: return ! $this->hasType() || $this->hasDefaultValue();
417: }
418:
419: try {
420: $this->getValue($object);
421:
422: return true;
423:
424: /** @phpstan-ignore catch.neverThrown */
425: } catch (Error $e) {
426: if (strpos($e->getMessage(), 'must not be accessed before initialization') !== false) {
427: return false;
428: }
429:
430: throw $e;
431: }
432: }
433:
434: public function isReadable(?string $scope, ?object $object = null): bool
435: {
436: if ($this->isDynamic()) {
437: return $this->isDynamicPropertyReadable($object);
438: }
439:
440: if ($this->isStatic() && $object !== null) {
441: throw new ReflectionException('null is expected as object argument for static properties');
442: }
443:
444: $property = $this;
445: $objectClass = null;
446:
447: if ($object !== null) {
448: [$property, $objectClass] = $this->getEffectivePropertyAndObjectClass($object);
449: }
450:
451: $propertyClass = $property->getImplementingClass();
452: $scopeClass = $property->reflectScope($scope);
453:
454: if (! self::isVisibleFromScope($propertyClass, $scopeClass, $property->getVisibility())) {
455: if ($property->isStatic()) {
456: return false;
457: }
458:
459: return $property->isReadableThroughMagic($objectClass ?? $propertyClass, $object);
460: }
461:
462: if ($property->isVirtual()) {
463: return $property->hasHook(ReflectionPropertyHookType::Get);
464: }
465:
466: if ($object !== null && ! $property->hasHook(ReflectionPropertyHookType::Get)) {
467: return $property->isInitializedOnObject($object);
468: }
469:
470: if ($property->isStatic()) {
471: return $property->isStaticInitialized();
472: }
473:
474: return true;
475: }
476:
477: public function isWritable(?string $scope, ?object $object = null): bool
478: {
479: if ($this->isDynamic()) {
480: return $this->isDynamicPropertyWritable($object);
481: }
482:
483: if ($this->isStatic() && $object !== null) {
484: throw new ReflectionException('null is expected as object argument for static properties');
485: }
486:
487: $property = $this;
488: $objectClass = null;
489:
490: if ($object !== null) {
491: [$property, $objectClass] = $this->getEffectivePropertyAndObjectClass($object);
492: }
493:
494: $propertyClass = $property->getImplementingClass();
495: $scopeClass = $property->reflectScope($scope);
496:
497: if (! self::isVisibleFromScope($propertyClass, $scopeClass, $property->getVisibility())) {
498: if ($property->isStatic()) {
499: return false;
500: }
501:
502: return ($objectClass ?? $propertyClass)->hasMethod('__set');
503: }
504:
505: if (! self::isVisibleFromScope($propertyClass, $scopeClass, $property->getSetVisibility())) {
506: return false;
507: }
508:
509: if ($property->isVirtual()) {
510: return $property->hasHook(ReflectionPropertyHookType::Set);
511: }
512:
513: return $object === null
514: || ! $property->isReadOnly()
515: || ! $property->isInitializedOnObject($object);
516: }
517:
518: public function isReadOnly(): bool
519: {
520: return ($this->modifiers & ReflectionPropertyAdapter::IS_READONLY_COMPATIBILITY)
521: || $this->getDeclaringClass()->isReadOnly();
522: }
523:
524: public function getDeclaringClass(): ReflectionClass
525: {
526: return $this->declaringClass ??= $this->reflector->reflectClass($this->declaringClassName);
527: }
528:
529: public function getImplementingClass(): ReflectionClass
530: {
531: return $this->implementingClass ??= $this->reflector->reflectClass($this->implementingClassName);
532: }
533:
534: /** @return non-empty-string|null */
535: public function getDocComment(): ?string
536: {
537: return $this->docComment;
538: }
539:
540: public function hasDefaultValue(): bool
541: {
542: return ! $this->hasType() || $this->default !== null;
543: }
544:
545: public function getDefaultValueExpression(): ?\PhpParser\Node\Expr
546: {
547: if (is_array($this->default)) {
548: $this->default = ExprCacheHelper::import($this->default);
549: }
550:
551: return $this->default;
552: }
553:
554: /**
555: * Get the default value of the property (as defined before constructor is
556: * called, when the property is defined)
557: * @return mixed
558: */
559: public function getDefaultValue()
560: {
561: if ($this->default === null) {
562: return null;
563: }
564:
565: if ($this->compiledDefaultValue === null) {
566: $this->compiledDefaultValue = (new CompileNodeToValue())->__invoke(
567: $this->getDefaultValueExpression(),
568: new CompilerContext(
569: $this->reflector,
570: $this,
571: ),
572: );
573: }
574:
575: /** @psalm-var scalar|array<scalar>|null $value */
576: $value = $this->compiledDefaultValue->value;
577:
578: return $value;
579: }
580:
581: public function isDeprecated(): bool
582: {
583: return DeprecatedHelper::isDeprecated($this);
584: }
585:
586: /**
587: * Get the line number that this property starts on.
588: *
589: * @return positive-int
590: *
591: * @throws CodeLocationMissing
592: */
593: public function getStartLine(): int
594: {
595: if ($this->startLine === null) {
596: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->getImplementingClass()->getName()));
597: }
598:
599: return $this->startLine;
600: }
601:
602: /**
603: * Get the line number that this property ends on.
604: *
605: * @return positive-int
606: *
607: * @throws CodeLocationMissing
608: */
609: public function getEndLine(): int
610: {
611: if ($this->endLine === null) {
612: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->getImplementingClass()->getName()));
613: }
614:
615: return $this->endLine;
616: }
617:
618: /**
619: * @return positive-int
620: *
621: * @throws CodeLocationMissing
622: */
623: public function getStartColumn(): int
624: {
625: if ($this->startColumn === null) {
626: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->getImplementingClass()->getName()));
627: }
628:
629: return $this->startColumn;
630: }
631:
632: /**
633: * @return positive-int
634: *
635: * @throws CodeLocationMissing
636: */
637: public function getEndColumn(): int
638: {
639: if ($this->endColumn === null) {
640: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->getImplementingClass()->getName()));
641: }
642:
643: return $this->endColumn;
644: }
645:
646: /** @return list<ReflectionAttribute> */
647: public function getAttributes(): array
648: {
649: return $this->attributes;
650: }
651:
652: /** @return list<ReflectionAttribute> */
653: public function getAttributesByName(string $name): array
654: {
655: $attributes = $this->getAttributes();
656:
657: if ($attributes === []) {
658: return [];
659: }
660:
661: return ReflectionAttributeHelper::filterAttributesByName($attributes, $name);
662: }
663:
664: /**
665: * @param class-string $className
666: *
667: * @return list<ReflectionAttribute>
668: */
669: public function getAttributesByInstance(string $className): array
670: {
671: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
672: }
673:
674: /**
675: * @throws ClassDoesNotExist
676: * @throws NoObjectProvided
677: * @throws ObjectNotInstanceOfClass
678: * @return mixed
679: */
680: public function getValue(?object $object = null)
681: {
682: $implementingClassName = $this->getImplementingClass()->getName();
683:
684: if ($this->isStatic()) {
685: $this->assertClassExist($implementingClassName);
686:
687: /** @psalm-suppress InvalidStringClass */
688: $closure = Closure::bind(fn (string $implementingClassName, string $propertyName) => $implementingClassName::${$propertyName}, null, $implementingClassName);
689:
690: assert($closure instanceof Closure);
691:
692: return $closure->__invoke($implementingClassName, $this->getName());
693: }
694:
695: $instance = $this->assertObject($object);
696:
697: $closure = Closure::bind(fn (object $instance, string $propertyName) => $instance->{$propertyName}, $instance, $implementingClassName);
698:
699: assert($closure instanceof Closure);
700:
701: return $closure->__invoke($instance, $this->getName());
702: }
703:
704: /**
705: * @throws ClassDoesNotExist
706: * @throws NoObjectProvided
707: * @throws NotAnObject
708: * @throws ObjectNotInstanceOfClass
709: * @param mixed $object
710: * @param mixed $value
711: */
712: public function setValue($object, $value = null): void
713: {
714: $implementingClassName = $this->getImplementingClass()->getName();
715:
716: if ($this->isStatic()) {
717: $this->assertClassExist($implementingClassName);
718:
719: $closure = Closure::bind(function (string $_implementingClassName, string $_propertyName, $value): void {
720: /** @psalm-suppress InvalidStringClass, MixedAssignment */
721: $_implementingClassName::${$_propertyName} = $value;
722: }, null, $implementingClassName);
723:
724: assert($closure instanceof Closure);
725:
726: $closure->__invoke($implementingClassName, $this->getName(), func_num_args() === 2 ? $value : $object);
727:
728: return;
729: }
730:
731: $instance = $this->assertObject($object);
732:
733: $closure = Closure::bind(function (object $instance, string $propertyName, $value): void {
734: $instance->{$propertyName} = $value;
735: }, $instance, $implementingClassName);
736:
737: assert($closure instanceof Closure);
738:
739: $closure->__invoke($instance, $this->getName(), $value);
740: }
741:
742: /**
743: * Does this property allow null?
744: */
745: public function allowsNull(): bool
746: {
747: return $this->type === null || $this->type->allowsNull();
748: }
749:
750: /**
751: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
752: */
753: private function createType(PropertyNode $node)
754: {
755: $type = $node->type;
756:
757: if ($type === null) {
758: return null;
759: }
760:
761: assert($type instanceof Node\Identifier || $type instanceof Node\Name || $type instanceof Node\NullableType || $type instanceof Node\UnionType || $type instanceof Node\IntersectionType);
762:
763: return ReflectionType::createFromNode($this->reflector, $this, $type);
764: }
765:
766: /**
767: * Get the ReflectionType instance representing the type declaration for
768: * this property
769: *
770: * (note: this has nothing to do with DocBlocks).
771: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
772: */
773: public function getType()
774: {
775: return $this->type;
776: }
777:
778: /**
779: * Does this property have a type declaration?
780: *
781: * (note: this has nothing to do with DocBlocks).
782: */
783: public function hasType(): bool
784: {
785: return $this->type !== null;
786: }
787:
788: public function isVirtual(): bool
789: {
790: $this->cachedVirtual ??= $this->createCachedVirtual();
791:
792: return $this->cachedVirtual;
793: }
794:
795: public function hasHooks(): bool
796: {
797: return $this->getHooks() !== [];
798: }
799:
800: /** @param ReflectionPropertyHookType::* $hookType */
801: public function hasHook(string $hookType): bool
802: {
803: return isset($this->getHooks()[$hookType]);
804: }
805:
806: /** @param ReflectionPropertyHookType::* $hookType */
807: public function getHook(string $hookType): ?\PHPStan\BetterReflection\Reflection\ReflectionMethod
808: {
809: return $this->getHooks()[$hookType] ?? null;
810: }
811:
812: /** @return array{get?: ReflectionMethod, set?: ReflectionMethod} */
813: public function getHooks(): array
814: {
815: $this->cachedHooks ??= $this->createCachedHooks();
816:
817: return $this->cachedHooks;
818: }
819:
820: /**
821: * @return array{0: self, 1: ReflectionClass}
822: *
823: * @throws ObjectNotInstanceOfClass
824: */
825: private function getEffectivePropertyAndObjectClass(object $object): array
826: {
827: $propertyClass = $this->getImplementingClass();
828:
829: if (! $propertyClass->isInstance($object)) {
830: throw ObjectNotInstanceOfClass::fromClassName($propertyClass->getName());
831: }
832:
833: if (get_class($object) === $propertyClass->getName()) {
834: // @infection-ignore-all ReturnRemoval: There's no difference, it's just optimization
835: return [$this, $propertyClass];
836: }
837:
838: /** @psalm-suppress ImpureMethodCall */
839: $objectClass = ReflectionClass::createFromInstance($object);
840:
841: if ($this->isPrivate()) {
842: return [$this, $objectClass];
843: }
844:
845: return [$objectClass->getProperty($this->name) ?? $this, $objectClass];
846: }
847:
848: private function isDynamicPropertyReadable(?object $object): bool
849: {
850: if ($object === null) {
851: return $this->getImplementingClass()->hasMethod('__get');
852: }
853:
854: if (property_exists($object, $this->name)) {
855: return true;
856: }
857:
858: if (! method_exists($object, '__get')) {
859: return false;
860: }
861:
862: if (! method_exists($object, '__isset')) {
863: return true;
864: }
865:
866: return isset($object->{$this->name});
867: }
868:
869: private function isDynamicPropertyWritable(?object $object): bool
870: {
871: if ($object === null) {
872: $class = $this->getImplementingClass();
873: } else {
874: /** @psalm-suppress ImpureMethodCall */
875: $class = ReflectionClass::createFromInstance($object);
876: }
877:
878: if (! $class->isReadOnly() && ! $class->isEnum()) {
879: return true;
880: }
881:
882: return $object === null
883: ? $class->hasMethod('__set')
884: : method_exists($object, '__set');
885: }
886:
887: private function isReadableThroughMagic(ReflectionClass $class, ?object $object): bool
888: {
889: if (! $class->hasMethod('__get')) {
890: return false;
891: }
892:
893: if ($object === null || ! $class->hasMethod('__isset')) {
894: return true;
895: }
896:
897: return isset($object->{$this->name});
898: }
899:
900: private function isInitializedOnObject(object $object): bool
901: {
902: /** @psalm-suppress ImpureMethodCall */
903: return (new CoreReflectionProperty($this->getImplementingClass()->getName(), $this->name))
904: ->isInitialized($object);
905: }
906:
907: private function isStaticInitialized(): bool
908: {
909: $className = $this->getImplementingClass()->getName();
910:
911: /** @psalm-suppress ImpureFunctionCall */
912: if (! class_exists($className, false)) {
913: return $this->isInitialized();
914: }
915:
916: /** @psalm-suppress ImpureMethodCall */
917: return (new CoreReflectionProperty($className, $this->name))->isInitialized();
918: }
919:
920: private function reflectScope(?string $scope): ?\PHPStan\BetterReflection\Reflection\ReflectionClass
921: {
922: if ($scope === null) {
923: return null;
924: }
925:
926: try {
927: /** @psalm-suppress ImpureMethodCall */
928: return $this->reflector->reflectClass($scope);
929: } catch (IdentifierNotFound $exception) {
930: throw new Error(sprintf('Class "%s" not found', $scope));
931: }
932: }
933:
934: /** @psalm-mutation-free */
935: private static function isVisibleFromScope(
936: ReflectionClass $propertyClass,
937: ?\PHPStan\BetterReflection\Reflection\ReflectionClass $scopeClass,
938: int $visibility
939: ): bool {
940: if ($visibility & CoreReflectionProperty::IS_PUBLIC) {
941: return true;
942: }
943:
944: if ($scopeClass === null) {
945: return false;
946: }
947:
948: $propertyClassName = $propertyClass->getName();
949: $scopeClassName = $scopeClass->getName();
950:
951: if (strcasecmp($propertyClassName, $scopeClassName) === 0) {
952: return true;
953: }
954:
955: if ($visibility & CoreReflectionProperty::IS_PRIVATE) {
956: return false;
957: }
958:
959: return $scopeClass->isSubclassOf($propertyClassName)
960: || $propertyClass->isSubclassOf($scopeClassName)
961: || $propertyClass->implementsInterface($scopeClassName);
962: }
963:
964: /** @psalm-mutation-free */
965: private function getVisibility(): int
966: {
967: if ($this->isPublic()) {
968: return CoreReflectionProperty::IS_PUBLIC;
969: }
970:
971: if ($this->isProtected()) {
972: return CoreReflectionProperty::IS_PROTECTED;
973: }
974:
975: return CoreReflectionProperty::IS_PRIVATE;
976: }
977:
978: /** @psalm-mutation-free */
979: private function getSetVisibility(): int
980: {
981: if ($this->isPrivateSet()) {
982: return CoreReflectionProperty::IS_PRIVATE;
983: }
984:
985: if ($this->isProtectedSet()) {
986: return CoreReflectionProperty::IS_PROTECTED;
987: }
988:
989: return $this->getVisibility();
990: }
991:
992: /**
993: * @param class-string $className
994: *
995: * @throws ClassDoesNotExist
996: */
997: private function assertClassExist(string $className): void
998: {
999: if (! ClassExistenceChecker::classExists($className, true) && ! ClassExistenceChecker::traitExists($className, true)) {
1000: throw new ClassDoesNotExist('Property cannot be retrieved as the class does not exist');
1001: }
1002: }
1003:
1004: /**
1005: * @throws NoObjectProvided
1006: * @throws NotAnObject
1007: * @throws ObjectNotInstanceOfClass
1008: *
1009: * @psalm-assert object $object
1010: * @param mixed $object
1011: */
1012: private function assertObject($object): object
1013: {
1014: if ($object === null) {
1015: throw NoObjectProvided::create();
1016: }
1017:
1018: if (! is_object($object)) {
1019: throw NotAnObject::fromNonObject($object);
1020: }
1021:
1022: $implementingClassName = $this->getImplementingClass()->getName();
1023:
1024: if (get_class($object) !== $implementingClassName) {
1025: throw ObjectNotInstanceOfClass::fromClassName($implementingClassName);
1026: }
1027:
1028: return $object;
1029: }
1030:
1031: /** @return int-mask-of<ReflectionPropertyAdapter::IS_*> */
1032: private function computeModifiers(PropertyNode $node): int
1033: {
1034: $modifiers = $node->isReadonly() ? ReflectionPropertyAdapter::IS_READONLY_COMPATIBILITY : 0;
1035: $modifiers += $node->isStatic() ? CoreReflectionProperty::IS_STATIC : 0;
1036: $modifiers += $node->isPrivate() ? CoreReflectionProperty::IS_PRIVATE : 0;
1037: $modifiers += ! $node->isPrivate() && $node->isPrivateSet() ? ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY : 0;
1038: $modifiers += $node->isProtected() ? ReflectionPropertyAdapter::IS_PROTECTED : 0;
1039: $modifiers += ! $node->isProtected() && $node->isProtectedSet() ? ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY : 0;
1040: $modifiers += $node->isPublic() ? ReflectionPropertyAdapter::IS_PUBLIC : 0;
1041: $modifiers += $node->isFinal() ? ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY : 0;
1042: $modifiers += $node->isAbstract() ? ReflectionPropertyAdapter::IS_ABSTRACT_COMPATIBILITY : 0;
1043:
1044: if (
1045: ! ($modifiers & ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY)
1046: && ($modifiers & ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY)
1047: ) {
1048: $modifiers += ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY;
1049: }
1050:
1051: if (
1052: ! ($modifiers & (ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY))
1053: && ! $node->isPublicSet()
1054: && $node->isPublic()
1055: && (
1056: ($modifiers & ReflectionPropertyAdapter::IS_READONLY_COMPATIBILITY)
1057: || $this->getDeclaringClass()->isReadOnly()
1058: )
1059: ) {
1060: $modifiers += ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY;
1061: }
1062:
1063: /** @phpstan-ignore return.type */
1064: return $modifiers;
1065: }
1066:
1067: private function computeImmediateVirtual(PropertyNode $node): bool
1068: {
1069: if ($node->hooks === []) {
1070: return false;
1071: }
1072:
1073: $setHook = null;
1074: $getHook = null;
1075:
1076: foreach ($node->hooks as $hook) {
1077: if ($hook->name->name === 'set') {
1078: $setHook = $hook;
1079: } elseif ($hook->name->name === 'get') {
1080: $getHook = $hook;
1081: }
1082: }
1083:
1084: if ($setHook !== null && ! $this->computeImmediateVirtualBasedOnHook($setHook)) {
1085: return false;
1086: }
1087:
1088: if ($getHook === null) {
1089: return true;
1090: }
1091:
1092: return $this->computeImmediateVirtualBasedOnHook($getHook);
1093: }
1094:
1095: private function computeImmediateVirtualBasedOnHook(Node\PropertyHook $hook): bool
1096: {
1097: $hookBody = $hook->getStmts();
1098:
1099: // Abstract property or property in interface
1100: if ($hookBody === null) {
1101: return true;
1102: }
1103:
1104: $visitor = new FindingVisitor(static fn (Node $node): bool => $node instanceof Node\Expr\PropertyFetch);
1105: $traverser = new NodeTraverser($visitor);
1106: $traverser->traverse($hookBody);
1107:
1108: foreach ($visitor->getFoundNodes() as $propertyFetchNode) {
1109: assert($propertyFetchNode instanceof Node\Expr\PropertyFetch);
1110:
1111: if (
1112: $propertyFetchNode->var instanceof Node\Expr\Variable
1113: && $propertyFetchNode->var->name === 'this'
1114: && $propertyFetchNode->name instanceof Node\Identifier
1115: && $propertyFetchNode->name->name === $this->name
1116: ) {
1117: return false;
1118: }
1119: }
1120:
1121: return true;
1122: }
1123:
1124: /** @return array{get?: ReflectionMethod, set?: ReflectionMethod} */
1125: private function createImmediateHooks(PropertyNode $node): array
1126: {
1127: $hooks = [];
1128:
1129: foreach ($node->hooks as $hook) {
1130: $hookName = $hook->name->name;
1131: assert($hookName === 'get' || $hookName === 'set');
1132:
1133: $hookType = $node->type;
1134: assert($hookType === null || $hookType instanceof Node\Identifier || $hookType instanceof Node\Name || $hookType instanceof Node\NullableType || $hookType instanceof Node\UnionType || $hookType instanceof Node\IntersectionType);
1135:
1136: $hooks[$hookName] = ReflectionMethod::createFromPropertyHook(
1137: $this->reflector,
1138: $hook,
1139: $this->getDeclaringClass()->getLocatedSource(),
1140: sprintf('$%s::%s', $this->name, $hookName),
1141: $hookType,
1142: $this->getDeclaringClass(),
1143: $this->getImplementingClass(),
1144: $this->getDeclaringClass(),
1145: $this,
1146: );
1147: }
1148:
1149: return $hooks;
1150: }
1151:
1152: private function createCachedVirtual(): bool
1153: {
1154: if (! $this->immediateVirtual) {
1155: return false;
1156: }
1157:
1158: return (($nullsafeVariable1 = $this->getParentProperty()) ? $nullsafeVariable1->isVirtual() : null) ?? true;
1159: }
1160:
1161: /** @return array{get?: ReflectionMethod, set?: ReflectionMethod} */
1162: private function createCachedHooks(): array
1163: {
1164: $hooks = $this->immediateHooks;
1165:
1166: // Just optimization - we don't need to check parent property when both hooks are defined in this class
1167: if (isset($hooks['get'], $hooks['set'])) {
1168: return $hooks;
1169: }
1170:
1171: $parentHooks = (($nullsafeVariable2 = $this->getParentProperty()) ? $nullsafeVariable2->getHooks() : null) ?? [];
1172:
1173: foreach ($parentHooks as $hookName => $parentHook) {
1174: if (isset($hooks[$hookName])) {
1175: continue;
1176: }
1177:
1178: $hooks[$hookName] = $parentHook;
1179: }
1180:
1181: return $hooks;
1182: }
1183:
1184: private function getParentProperty(): ?\PHPStan\BetterReflection\Reflection\ReflectionProperty
1185: {
1186: return ($nullsafeVariable3 = $this->getDeclaringClass()->getParentClass()) ? $nullsafeVariable3->getProperty($this->name) : null;
1187: }
1188: }
1189: